В четвертом издании популярного руководства даны основы программирования в операционной системе Linux. Рассмотрены: использование библиотек C/C++ и стан­дартных средств разработки, организация системных вызовов, файловый ввод/вывод, взаимодействие процессов, программирование средствами командной оболочки, создание графических пользовательских интерфейсов с помощью инструментальных средств GTK+ или Qt, применение сокетов и др. Описана компиляция программ, их компоновка c библиотеками и работа с терминальным вводом/выводом. Даны приемы написания приложений в средах GNOME® и KDE®, хранения данных с использованием СУБД MySQL® и отладки программ. Книга хорошо структурирована, что делает обучение легким и быстрым. Для начинающих Linux-программистов

Нейл Мэтью

Ричард Стоунс

Основы программирования в Linux

4-е издание

Об авторах

Нейл Мэтью (Neil Matthew) интересуется компьютерами и пишет для них программы с 1974 г. Выпускник университета г. Ноттингема по специальности "Математика", Нейл по-настоящему увлекается языками программирования и любит искать новые пути решения компьютерных проблем. Им разработаны системы программирования на языках BCPL, FP (Functional programming), Lisp, Prolog и структурированном BASIC. Он даже написал эмулятор микропроцессора 6502 для выполнения в системах UNIX программ для микрокомпьютера ВВС.

Что касается опыта работы в UNIX, начиная с конца 1970 гг., Нейл испробовал все варианты, включая BSD UNIX, AT&T System V, Sun Solaris, IBM AIX, многие другие и, конечно, Linux. Он может утверждать, что занимается ОС Linux с августа 1993 г., когда обзавелся дистрибутивом из Канады Software Landing (SLS) на дискетах с версией ядра 0.99.11. Он применял компьютеры на базе Linux дома и на работе для осваивания языков С, С++, Icon, Prolog, Tcl и Java.

Все "домашние" проекты Нейла разработаны на базе Linux. Он утверждает, что Linux гораздо проще, поскольку поддерживает множество функциональных возможностей других систем, поэтому программы, предназначенные для систем BSD и System V, как правило, компилируются с небольшими изменениями или вообще без каких-либо изменений.

Сейчас Нейл работает в компании Celesio AG как архитектор ПО компании (Enterprise Architect), специализирующийся на разработке стратегии информационных технологий. У него есть профессиональный опыт технического консультирования, разработки программного обеспечения и контроля качества. Нейл также писал программы на языках С и С++ для встроенных систем реального времени.

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

Ричард Стоунс (Richard Stones) начал программировать в школе (раньше, чем он может вспомнить) на микрокомпьютере ВВС, оснащенном микропроцессором 6502, который с помощью нескольких запчастей продолжал функционировать следующие 15 лет. Он закончил университет г. Ноттингема по специальности "Электроника", но решил, что программное обеспечение увлекательнее.

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

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

Будучи отчасти лингвистом в программировании он писал программы на разных ассемблерах, на чистом, патентованном языке телекоммуникаций, названном SL-1, нескольких диалектах языка FORTRAN, языках Pascal, Perl, SQL и чуть-чуть на Python, С++ и С. (Под давлением он даже признался, что одно время считался не без оснований специалистом в Visual Basic, но старается не афишировать это временное помрачение рассудка.)

Рик живет в деревне графства Лестершир в Англии с женой Анной, детьми Дженнифер и Эндрю и кошкой. В свободное от работы время он увлекается классической музыкой, особенно старинной духовной музыкой, и фотографией и прилагает максимум усилий, чтобы найти время для игры на пианино. 

Благодарности

Авторы хотели бы поблагодарить многих людей, помогавших сделать возможным издание книги.

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

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

Мы хотели бы выразить признательность сотрудникам издательства Wiley, которые помогли подготовить это четвертое издание к печати. Спасибо Кэрол Лонг (Carol Long) за запуск этого процесса и улаживание проблем, связанных с контрактами, особая благодарность Cape Шлаер (Sara Shlaer) за исключительную редакторскую работу и Тимоти Борончику (Timothy Boronczyk) за отличные технические рецензии, Мы также хотим поблагодарить Дженни Ватсон (Jenny Watson) за поиск средств для оплаты неожиданно возникавших дополнительных расходов и сопровождение книги на всех административных уровнях, Биллу Бартону (Bill Barton) за обеспечение надлежащих организации и презентации и Киму Коферу (Kim Cofer) за тщательную корректуру. Мы также очень признательны Эрику Фостеру-Джонсону (Eric Foster-Johnson) за его фантастическую работу над главами 16 и 17. Мы можем сказать, что благодаря стараниям всех вас книга стала лучше.

Мы также хотели бы поблагодарить наших работодателей, компании Scientific Generics, Mobicom и Celesio за поддержку во время подготовки всех четырех изданий книги.

В заключение нам хотелось бы выразить глубокую признательность двум важнейшим инициаторам, сделавшим возможным появление книги. Во-первых, Ричарду Столлмену (Richard Stallman) за отличные средства проекта GNU и идею среды со свободно распространяемым программным обеспечением, ставшей в наши дни реальностью благодаря GNU/Linux, и во-вторых, Линусу Торвальдсу (Linus Torvalds) за начатую и продолженную им совместную разработку, которая дает нам все улучшающееся ядро системы Linux.

Предисловие

У всех программистов есть своя груда записей и черновиков. Они собирают собственные примеры текстов программ, накопившиеся за время героических погружений в многочисленные руководства или добытые из сети Usenet, в которой порой даже дураки боятся блуждать. (Другая точка зрения состоит в том, что у всех дураков свободный доступ к Usenet, и они используют ее безостановочно.) Поэтому довольно странно, что так мало книг выпущено в подобном стиле. В интерактивном мире существует множество коротких документов, касающихся конкретных проблем программирования и администрирования по существу. В рамках проекта по созданию документации Linux выпущено множество документов, посвященных самым разным темам, начиная с установки ОС Linux и Windows на одной машине и заканчивая написанием вашей виртуальной машины Java для Linux. На самом деле, загляните на Web-сайт Linux Documentation Project (проект документации Linux) по адресу http://www.tldp.org.

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

Данное издание книги было проверено и исправлено в соответствии с современным уровнем разработок в ОС Linux.

Алан Кокс (Alan Сох)

Введение

Рады предложить вам легкое в использовании руководство по разработке программ для Linux и других UNIX-подобных операционных систем.

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

Для кого эта книга?

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

Мы полагаем, что у вас есть некоторый опыт программирования на языках С и/или С++ в ОС Windows или какой-нибудь другой операционной системе, но мы старались сохранить простоту приведенных в книге примеров, чтобы для их понимания не требовалось слишком высокой квалификации в программировании на С. Все явные сопоставления методов программирования в Linux с приемами программирования на языках C/C++ отмечены в тексте книги.

Примечание

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

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

Чему посвящена книга?

У книги есть ряд задач, перечисленных далее.

□ Научить применению стандартных библиотек языка С в ОС Linux и других средств, описанных в разных стандартах Linux и UNIX.

□ Показать, как использовать большинство стандартных средств разработки Linux.

□ Дать краткий обзор способов хранения данных под управлением Linux с помощью СУБД DBM и MySQL.

□ Показать, как создавать графические интерфейсы пользователя на базе графической системы X Window System. Мы воспользуемся библиотеками GTK (основы графической среды GNOME) и Qt (основы графической среды KDE).

□ Поддержать вас и дать вам практические навыки разработки собственных реальных приложений.

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

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

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

После этого мы обсудим управление данными. Знакомство с библиотекой базы данных dbm, к которой мы обратимся в нескольких последующих главах, — достаточное основание для переделки приложения, но на этот раз вместе с проектом. В следующей главе рассматривается хранение данных в реляционной базе данных средствами СУРБД MySQL и позже мы также повторно применим эти методы хранения данных, поэтому вы сможете сравнить разные способы управления данными. Размер новых версий приложения таков, что нам далее придется иметь дело с такими практическими задачами, как отладка, контроль исходного текста программы, распространение программного обеспечения и make-файлы.

Вы также узнаете, как могут взаимодействовать разные процессы в ОС Linux и как программы в Linux могут применять сокеты для поддержки сетевых соединений разных машин на базе протоколов TCP/IP, включая проблемы, касающиеся связи машин с процессорами разной архитектуры.

После изложения основ программирования в Linux мы обсуждаем создание программ в графическом режиме. Этому посвящены две главы, в которых сначала рассматривается комплект инструментальных средств GTK+, лежащий в основе графической среды GNOME, а затем комплект Qt, лежащий в основе графической среды KDE.

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

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

Что вам потребуется для использования книги?

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

Существуют варианты Linux для самых разных систем. Адаптируемость Linux такова, что предприимчивые люди заставляют ее работать в том или ином виде на любом оборудовании, имеющем процессор! Примеры включают системы на базе процессоров Alpha, ARM, IBM Cell, Itanium, PA-RISC, PowerPC, SPARC, SuperH и ЦП 68k, а также на базе различных процессоров класса х86 с 32- и 64-разрядными версиями.

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

Работая над книгой, мы сначала главным образом использовали системы на базе процессоров x86, хотя мало что из описанного в книге характерно только для х86. Несмотря на то, что можно успешно запускать Linux на PC 486 с 8 Мбайт RAM, для успешной работы современного дистрибутива Linux и выполнения примеров из этой книги мы советуем выбрать современную версию одного из наиболее популярных дистрибутивов Linux, например Fedora, openSUSE или Ubuntu, и проверить их аппаратные рекомендации.

Что касается требований к программному обеспечению, мы полагаем, что вы используете современную версию предпочитаемого вами дистрибутива Linux и, чтобы поддерживать систему на современном уровне и иметь самые свежие исправления найденных ошибок, применяете текущий набор обновлений, которые большинство поставщиков делают доступными интерактивно в виде автоматических обновлений. Linux и комплект инструментальных средств проекта GNU выпускаются на условиях GNU General Public License (GPL) (Общедоступной лицензии проекта GNU). Большинство других компонентов типичного дистрибутива Linux ссылаются либо на GPL, либо на одну из множества других лицензий Open Source (открытый или свободно распространяемый программный код), и это означает, что у них есть определенные характеристики, одна из которых — свобода. У них всегда есть исходный программный код, и никто не может отнять эту свободу. Дополнительную информацию о GPL см. на Web-сайте http://www.gnu.org/licenses/, а определение Open Source и разные применяемые лицензии — на Web-сайте http://www.opensource.org. В случае GNU/Linux у вас всегда будет возможность технической поддержки либо благодаря самостоятельной работе с исходным программным кодом, либо за счет найма стороннего специалиста или обращения к одному из поставщиков, предлагающих платную техническую поддержку.

Исходный программный код

Для работы с примерами книги можно ввести программный код вручную или воспользоваться сопроводительными файлами с исходным текстом примеров. Весь программный код, применяемый в книге, можно найти на Web-сайте http://www.wrox.com. Открыв главную страницу сайта, просто найдите заголовок книги (либо с помощью поля Search (Поиск), либо используя один из списков заголовков) и на странице с описанием книги щелкните кнопкой мыши ссылку Download Code для того, чтобы получить весь программный код примеров.

Примечание

Поскольку у многих книг похожие заголовки, легче всего найти нужную книгу по номеру ISBN (International Standard Book Number); ISBN этой книги (оригинальной) — 978-0-470-14762-7.

После загрузки программного кода из Интернета просто распакуйте его своей любимой программой сжатия. Вы также можете перейти на главную страницу загрузки программного кода издательства Wrox http://www.wrox.com/dynamic/books/download.aspx, для того чтобы просмотреть код к данной книге и ко всем остальным книгам издательства.

Замечание, касающееся программного кода примеров

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

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

Общедоступная лицензия проекта GNU

Исходный программный код книги сделан доступным на условиях Общедоступной лицензии проекта GNU версии 2 (GNU General Public License, version 2), опубликованной на Web-странице http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. Приведенное далее положение о разрешении и правах применяется ко всему программному коду данной книги.

This program is free software; you can redistribute it and/or modify

it under the terms of the GNU General Public License as published by

the Free Software Foundation; either version 2 of the License, or

(at your option) any later version.

(Это программа — свободно распространяемое программное обеспечение; вы можете

распространять ее и/или изменять на условиях Общедоступной лицензии GNU,

опубликованной Фондом свободного программного обеспечения;

либо версии 2 этой лицензии, либо (по вашему усмотрению) любой более свежей версии.)

This program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.

(Эта программа распространяется в расчете на ее полезность, но без каких-либо

гарантий, даже без подразумеваемой гарантии ТОВАРНОГО СОСТОЯНИЯ ПРИ ПРОДАЖЕ И

ПРИГОДНОСТИ ДЛЯ ИСПОЛЬЗОВАНИЯ В КОНКРЕТНЫХ ЦЕЛЯХ. Более подробную информацию

см. в Общедоступной лицензии проекта GNU.)

You should have received a copy of the GNU General Public License

along with this program; if not, write to the Free Software

Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

(Вы должны были получить копию Общедоступной лицензии GNU вместе с этой

программой; если этого не произошло, напишите в Фонд свободного программного

обеспечения по адресу Free Software Foundation, Inc., 59 Temple Place, Suite

330, Boston, MA 02111-1307 USA)

Стилевое оформление, принятое в книге

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

Примечание

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

Когда вводятся важные понятия, мы выделяем их курсивом. Символы, которые вы должны ввести, выделяются жирным моноширинным шрифтом. Элементы интерфейса выделены полужирным шрифтом. Комбинации клавиш обозначаются следующим образом: <Ctrl>+<A>

Программный код и терминальные сеансы мы приводим тремя разными способами:

$ who

root tty1 Sep 10 16:12

rick tty2 Sep 10 16:10

Верхняя строка приведенного кода — это командная строка, а остальные строки отображаются в обычном стиле. Знак $ — приглашение (если для ввода команды требуется суперпользователь, приглашение обозначается знаком #); жирным шрифтом помечается текст, который вы должны ввести, и для выполнения команды следует нажать клавишу <Enter> (или <Return>). Любой последующий текст, набранный тем же шрифтом, но без выделения жирным, — это вывод обозначенной жирным шрифтом команды. В приведенном примере вы вводите команду who и видите ее вывод в двух строках, расположенных под ней.

Прототипы функций и структуры, определенные в системе Linux, приводятся жирным шрифтом, как показано далее:

#include <stdio.h>

int printf(const char *format, ...);

В программном коде наших примеров строки с выделенным фоном указывают на новый важный материал, например, так:

/* Это новый материал, и соответствующий код выглядит так. */

если код выглядит так, как показано далее (без выделения фоном), он менее важен:

/* Этот код уже встречался, и он выглядит так. */

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

/* Программный код примера */

/* Это строка завершения. */ 

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

/* Программный код примера. */

/* В эти строки */

/* добавляется новый код */

/* Это строка завершения. */

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

Ошибки

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

Для поиска страницы с ошибками, найденными в этой книге, перейдите на Web-сайт http://www.wrox.corn и найдите заголовок с помощью поля Search (Поиск) или одного из списков заголовков. Далее на странице с выходными данными книги щелкните кнопкой мыши ссылку Errata (Ошибки). Вы попадете на страницу, отображающую все ошибки, представленные на рассмотрение и опубликованные редакторами издательства Wrox. На Web-странице www.wrox.com/misc-pages/booklist.shtml можно найти полный список книг, включающий ссылки на ошибки, найденные в каждой книге.

Если вы не обнаружили "свою" ошибку на странице Errata (Ошибки), перейдите на страницу www.wrox.com/contact/techsupport.shtml и заполните форму для отправки нам найденной вами ошибки. Мы проверим присланную информацию и, если согласимся с ней, опубликуем сообщение на странице с ошибками, найденными в книге, и исправим ее в последующих изданиях книги.

Сайт p2p.wrox.com

Для обмена мнениями с авторами и такими же, как вы, читателями присоединяйтесь к форумам Р2Р (Programmer to Programmer) на Web-сайте p2p.wrox.com. Форумы — это система на основе Web-технологии, предназначенная для отправки вашего сообщения, относящегося к книгам издательства Wrox и родственным технологиям, и обмена мнениями с другими читателями и пользователями этих технологий. Форумы предлагают функцию подписки для отправки вам по электронной почте по мере поступления новых сообщений, относящихся к выбранным вами и интересующих вас темам. На этих форумах представлены авторы и редакторы Wrox и другие специалисты, работающие в области информационных технологий.

На Web-сайте http://p2p.wrox.com вы найдете ряд разных форумов, которые помогут вам не только во время чтения книги, но и в процессе разработки ваших собственных приложений. Для присоединения к форумам выполните следующие действия:

1. Перейдите на Web-сайт p2p.wrox.com и щелкните кнопкой мыши ссылку Register (Зарегистрироваться).

2. Прочтите условия пользования и щелкните мышью кнопку Agree (Принять).

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

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

Примечание

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

После присоединения к форуму вы можете посылать новые сообщения и отвечать на сообщения, посланные другими пользователями. Читать сообщения можно будет в любое время, находясь в Web-пространстве. Если вы хотите получать по электронной почте новые сообщения, появляющиеся на конкретном форуме, щелкните мышью пиктограмму Subscribe to this Forum (Подписаться на этот форум), расположенную рядом с именем форума в списке форумов.

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

Глава 1

Приступая к работе

В этой главе вы узнаете, что такое ОС Linux и как она связана со своим прообразом — ОС UNIX, познакомитесь с функциями и средствами, предоставляемыми средой разработки программ в ОС Linux, и напишите и выполните свою первую программу. Попутно вы получите представление о:

□ UNIX, Linux и проекте GNU;

□ программах и языках программирования в ОС Linux;

□ способах поиска ресурсов разработки;

□ статических и совместно используемых библиотеках;

□ теоретических основах ОС UNIX.

Введение в UNIX, Linux и проект GNU

В последние годы ОС Linux стала заметным явлением. И дня не проходит без того или иного упоминания Linux в электронных средствах массовой информации. Мы потеряли счет приложениям, которые стали доступны в ОС Linux и внедрившим ее организациям, включая некоторые министерства и городские администрации. Основные поставщики компьютерного оборудования, такие как корпорации IBM и Dell, поддерживают ОС Linux, а крупнейшие разработчики программного обеспечения, например компания Oracle, обеспечивают выполнение своих программ в ОС Linux. Linux стала по-настоящему конкурентоспособной операционной системой, особенно на серверном рынке.

Своим успехом она обязана системам и приложениям — предшественникам: ОС UNIX и программному обеспечению GNU. В этом разделе рассматривается, как появилась ОС Linux и каковы ее корни. 

Что такое ОС UNIX?

Операционная система UNIX первоначально была разработана в компании Bell Laboratories, бывшей в то время частью телекоммуникационного гиганта, компании AT&T. Разработанная в 1970-х гг. для мини-компьютеров PDP корпорации Digital Equipment ОС UNIX стала очень популярной многопользовательской, многозадачной операционной системой для самых разных аппаратных платформ, начиная с рабочих станций PC и заканчивая многопроцессорными серверами и суперкомпьютерами.

Краткая история ОС UNIX

Строго говоря, UNIX — это торговое название, контролируемое организацией Open Group и относящееся к компьютерной операционной системе, соответствующей определенной спецификации. В этой спецификации, именуемой "The Single UNIX Specification" ("Единая спецификация UNIX"), определены имена, интерфейсы и поведение всех обязательных функций операционной системы UNIX. Данная спецификация в значительной степени представляет собой расширенный набор более ранних спецификаций, стандартов Р1003 или POSIX (Portable Operating System Interface, интерфейс переносимой операционной системы), разработанных IEEE (Institute of Electrical and Electronic Engineers, Институт инженеров по электротехнике и радиоэлектронике).

Существует много коммерческих UNIX-подобных систем, таких как AIX корпорации IBM, UX компании HP и Solaris компании Sun Microsystems. Некоторые системы, например FreeBSD и Linux, свободно распространяются. В настоящее время спецификации Open Group удовлетворяют лишь несколько операционных систем, что позволяет предлагать их на рынке с названием UNIX.

В прошлом совместимость разных систем UNIX была реальной проблемой, хотя стандарт POSIX и оказывал неоценимую помощь в ее решении. В наши дни следование нескольким простым правилам сделало возможным создание приложений, работающих под управлением всех UNIX и UNIX-подобных систем. Более подробную информацию о стандартах ОС Linux и UNIX вы сможете найти в главе 18.

Идеология UNIX

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

В операционной системе UNIX, а значит и в Linux, поощряется определенный стиль программирования. Далее перечислены некоторые характеристики, общие для типовых программ и систем UNIX.

□ Простота. Многие из наиболее полезных утилит UNIX очень просты и как результат малы и понятны. KISS (Keep It Small and Simple, сохраняйте программу маленькой и простой) — отличный подход, которому следует научиться. Чем больше и сложнее система, тем наверняка в ней больше сложных ошибок, и отладка превращается в тяжелую работу, которой хотелось бы избежать. 

□ Узкая направленность. Зачастую лучше сделать программу, хорошо выполняющую одну задачу, чем включать в каждую функцию полный набор нужного и ненужного. "Раздутую" программу трудно использовать и поддерживать ее работоспособность. Одноцелевые программы легче усовершенствовать при появлении улучшенных алгоритмов или интерфейсов. В ОС UNIX при необходимости выполнения трудных задач чаще комбинируются маленькие утилиты, чем делается попытка в одной большой программе предусмотреть все потребности пользователя.

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

□ Фильтры. Многие приложения UNIX могут применяться как фильтры. Они преобразуют свой ввод и формируют вывод. Как вы увидите в дальнейшем, ОС UNIX обладает функциональными возможностями, позволяющими разрабатывать очень сложные приложения из других UNIX-программ путем комбинирования их оригинальными способами. Конечно, подобное многократное использование возможно благодаря методам разработки, упоминавшимся ранее.

□ Открытые файловые форматы. Наиболее удачные и популярные UNIX- программы применяют файлы конфигурации и файлы данных в виде обычного текста ASCII или файла на языке XML. Если в разрабатываемой вами программе можно использовать любой из этих форматов — это хороший выбор. Он позволит другим пользователям применить стандартные средства при изменении или поиске элементов конфигурации и разрабатывать новые средства для выполнения новых функций обработки файлов данных. Хорошим примером такого подхода может служить система перекрестных ссылок исходного кода ctags, записывающая сведения о местоположении символа в. виде регулярного выражения, подходящего для использования программами поиска.

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

Что такое Linux?

Как вы уже, возможно, знаете, Linux — это свободно распространяемая реализация UNIX-подобного ядра, низкоуровневой сердцевины операционной системы. Поскольку прообразом ОС Linux стала система UNIX, Linux- и UNIX-программы очень похожи. В действительности почти все программы, написанные для ОС UNIX, могут быть скомпилированы и выполнены в ОС Linux. Кроме того, некоторые коммерческие приложения, продаваемые для коммерческих версий UNIX, могут выполняться без изменения их двоичного кода в системах под управлением Linux.

ОС Linux была разработана Линусом Торвальдсом (Linus Torvalds) из Университета г. Хельсинки совместно с программистами UNIX, оказывавшими ему помощь по Интернету. Работа начиналась как хобби, а вдохновителем стала ОС Minix Энди Таненбаума (Andy Tanenbaum), маленькая UNIX-подобная система. Со временем Linux выросла, превратившись в сложную самостоятельную систему. Ее цель — отказ от патентованного кода и применение только свободно распространяемого программного кода.

В настоящее время ОС Linux существует для широкого набора компьютерных систем с разными типами процессоров, включая PC на 16- и 32-битных процессорах Intel x86 и совместимых с ними процессорах; рабочие станции и серверы на процессорах Sun SPARC, IBM PowerPC, AMD Opteron и Intel Itanium и даже некоторые карманные компьютеры PDA и игровые приставки Playstation 2 и 3 фирмы Sony. Если у устройства есть процессор, кто-то где-нибудь пытается добыть ОС Linux, выполняющуюся на этом процессоре!

Проект GNU и Фонд свободного ПО 

ОС Linux обязана своим существованием совместным усилиям множества людей. Ядро операционной системы само по себе образует лишь малую часть пригодной к использованию системы разработки. Коммерческие системы UNIX традиционно снабжаются приложениями, обеспечивающими системные сервисы и средства. Для систем Linux подобные дополнительные программы написаны множеством разных программистов и распространяются они свободно.

Linux-сообщество (совместно с другими людьми) поддерживает идею свободного программного обеспечения (ПО), т.е. свободного от ограничений и подчиняющегося Общедоступной лицензии проекта GNU (GNU General Public License, GPL). (GNU означает GNU's Not UNIX (GNU не UNIX).) Несмотря на то, что получение программного обеспечения может быть небесплатным, это ПО может использоваться как угодно и обычно распространяется в виде исходного программного кода.

Фонд свободного программного обеспечения (Free Software Foundation) был организован Ричардом Столлменом (Richard Stallman) — автором GNU Emacs, одного из самых известных текстовых редакторов для ОС UNIX и других систем. Столлмен — автор концепции свободного программного обеспечения и организатор проекта GNU, попытки создания операционной системы и среды разработки, совместимой с ОС UNIX, но не подверженной ограничениям, связанным с торговой маркой UNIX и предоставлением исходного программного кода. В любой момент может оказаться, что проект GNU сильно отличается от UNIX способами поддержки аппаратных средств и управления исполняемыми программами, но он будет продолжать поддерживать приложения в стиле UNIX.

Проект GNU уже снабдил программистское сообщество множеством приложений, сильно напоминающих, компоненты, входящие в системы UNIX. Все эти программы, называемые программным обеспечением GNU, распространяются в соответствии с Общедоступной лицензией GNU (GPL), копию которой можно найти на сайте http://www.gnu.org. В этой лицензии вводится понятие "авторского "лева" (copyleft)" (в противоположность авторскому праву ("copyright")). Авторское "лево" задумано как препятствие установлению каких-либо ограничений на использование свободного программного обеспечения.

Далее приведены основные примеры ПО проекта GNU, распространяемого в соответствии с лицензией GPL:

□ пакет компиляторов GCC (GNU Compiler Collection), включающий компилятор GNU С;

□ G++ — компилятор С++, включающий как часть GCC;

□ GDB — отладчик на уровне исходного кода;

□ GNU make — версия UNIX-автосборщика make;

□ Bison — генератор синтаксических анализаторов, совместимый с генератором компиляторов UNIX yacc;

□ bash — командная оболочка;

□ GNU Emacs — текстовый редактор и среда разработки.

Кроме того, было разработано и распространено на принципах свободного ПО и под контролем лицензии GPL множество других пакетов, включая электронные таблицы, средства управления программным кодом, компиляторы, интерпретаторы, интернет-средства, программы обработки графических объектов, например, графический редактор Gimp и две законченные объектно-ориентированные среды разработки: GNOME и KDE. Мы обсудим GNOME и KDE в главах 16 и 17.

Сейчас существует такое множество доступного свободного программного обеспечения, что если добавить к нему ядро Linux, можно сказать, что благодаря Linux достигнута основная цель проекта GNU — свободная UNIX-подобная система. Признавая вклад программного обеспечения проекта GNU, многие люди теперь, как правило, называют Linux-системы GNU/Linux.

Более подробную информацию о концепции свободного программного обеспечения можно получить на Web-сайте http://www.gnu.org.

Дистрибутивы Linux

Как мы уже упоминали, Linux — это только ядро. Вы можете получить исходный программный код ядра, откомпилировать его и установить на машину, а затем получить и установить много другого свободного программного обеспечения для завершения установки ОС Linux. Такие установки часто называют системами Linux, т.к. они содержат много программ помимо ядра. Большинство утилит приходит от проекта GNU Фонда свободного ПО. 

Понятно, что создание системы Linux только из исходного программного кода — трудное дело. К счастью, многие люди подготовили готовые к установке дистрибутивы (часто называемые разновидностями (flavor)), обычно загружаемые из Интернета или с CD/DVD-накопителей и содержащие не только ядро, но и множество других программных средств и утилит. Часто в их состав входит реализация X Window System — графической оболочки, общей для множества систем UNIX. Дистрибутивы обычно снабжаются программой установки и дополнительной документацией (как правило, все на компакт-дисках), чтобы помочь вам установить собственную систему Linux. К некоторым хорошо известным дистрибутивам, в особенности для семейства процессоров Intel х86, относятся дистрибутивы Red Hat Enterprise Linux и его усовершенствованный сообществом родственник Fedora, Novell SUSE Linux и свободно распространяемый вариант openSUSE, Ubuntu Linux, Slackware, Gentoo и Debian GNU/Linux. Подробную информацию о множестве других дистрибутивов можно найти на Web-сайте DistroWatch по адресу http://distrowatch.com.

Программирование в ОС Linux

Многие думают, что программирование в Linux означает применение языка программирования С. Известно, что ОС UNIX первоначально была написана на С и что большинство UNIX-приложений были написаны на языке С. Но для программистов ОС Linux, или UNIX, С — не единственно возможный вариант. Далее в книге мы назовем пару альтернатив.

Примечание

На самом деле первая версия UNIX была написана в 1969 г. на ассемблере PDP 7. Язык С был задуман Деннисом Ритчи (Dennis Ritchie) примерно в это время, и в 1973 г. он вместе с Кеном Томпсоном (Ken Tompson) по существу переписал на С все ядро UNIX, совершив настоящий подвиг в эпоху разработки системного программного обеспечения на языке ассемблера.

В системах Linux доступен широкий диапазон языков программирования, многие из них свободно распространяются и есть на компакт-дисках или в архивах на FTP- сайтах в Интернете. Далее перечислена часть языков программирования, доступных программистам Linux:

□ Ada;

□ С;

□ С++;

□ Eiffel;

□ Forth;

□ Fortran;

□ Icon;

□ Java;

□ JavaScript;

□ Lisp;

□ Modula 2;

□ Modula 3;

□ Oberon;

□ Objective С;

□ Pascal; 

□ Perl;

□ Prolog;

□ PostScript;

□ Python;

□ Ruby;

□ Smalltalk;

□ PHP;

□ Tcl/Tk;

□ Bourne Shell.

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

Linux-программы

Linux-приложения представлены файлами двух типов: исполняемыми (executable) и сценариями или пакетными файлами (script). Исполняемые файлы — это программы, которые могут непосредственно выполняться на компьютере; они соответствуют файлам ОС Windows с расширением exe. Сценарии или пакетные файлы — это наборы команд для выполнения другой программой, интерпретатором. Они соответствуют в ОС Windows файлам с расширением bat или cmd или интерпретируемым программам на языке Basic.

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

В процессе регистрации в системе Linux вы взаимодействуете с программой командной оболочки (часто bash), которая запускает программы так же, как это делает оболочка командной строки в ОС Windows. Она находит запрашиваемые вами программы по имени, выполняя поиск файла с тем же именем в заданном наборе каталогов. Каталоги, предназначенные для поиска, хранятся в переменной оболочки PATH, так же как в ОС Windows. Путь поиска (который вы можете пополнять) настраивается вашим системным администратором и обычно содержит стандартные каталоги, в которых сохраняются системные программы. К ним относятся:

□ /bin — бинарные файлы (binaries), программы, применяемые для загрузки системы;

□ /usr/bin — пользовательские библиотеки, стандартные программы, доступные пользователям;

□ /usr/local/bin — локальные библиотеки, программы, относящиеся к этапу инициализации.

Если войти в систему как администратор, например с именем root, можно использовать переменную PATH, которая включает каталоги с хранящимися системными программами, такие как /sbin и /usr/sbin.

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

Примечание

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

Обратите внимание на то, что в ОС Linux, как и UNIX, для разделения отдельных элементов в переменной PATH применяется символ двоеточия (:) в отличие от символа точки с запятой, используемого в ОС MS-DOS и Windows. (ОС UNIX сделала выбор первой, поэтому спрашивайте, почему отличается Windows, а не почему в UNIX все не так!) Далее приведен пример переменной PATH:

/usr/local/bin:/bin:/usr/bin:.:/home/neil/bin:/usr/X11R6/bin

В этой переменной PATH содержатся каталоги для хранения стандартных программ, текущий каталог (.), исходный каталог пользователя и каталог графической оболочки X Window System.

Запомните, в ОС Linux используется прямой слэш (/) для отделения имен каталогов в полном имени файла в отличие от обратного слэша (\), применяемого в ОС Windows. И снова ОС UNIX выбирала первой.

Текстовые редакторы

Для ввода и набора примеров программного кода, приведенных в книге, вам понадобится текстовый редактор. В типовых системах Linux есть большой выбор таких программ. У многих пользователей популярен редактор vi.

Оба автора предпочитают Emacs, поэтому мы предлагаем потратить немного времени на знакомство с основными функциями этого мощного редактора. Почти во все дистрибутивы ОС Linux Emacs включен как необязательный пакет, который можно установить. Кроме того, вы можете получить его на Web-сайте GNU по адресу http://www.gnu.org или же взять версию для графических сред разработки на Web-сайте XEmacs по адресу http://www.xemacs.org.

Для того чтобы узнать больше о редакторе Emacs, можно воспользоваться его интерактивным средством обучения. Начните с выполнения команды emacs, затем нажмите комбинацию клавиш <Ctrl>+<H> с последующим вводом символа t для доступа к этому средству. У редактора Emacs есть также полное руководство. Для получения дополнительной информации о нем в редакторе Emacs нажмите комбинацию клавиш <Ctrl>+<H> с последующим вводом символа i. В некоторых версиях Emacs может быть меню, предоставляющее доступ к средству обучения и полному руководству.

Компилятор языка С

В системах, соответствующих стандарту POSIX, компилятор языка С называется с89. Раньше компилятор языка С назывался просто сс. Шли годы, разные поставщики продавали UNIX-подобные системы с компиляторами С, обладающими разными функциями и параметрами, но очень часто все также названными сс.

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

Вместо этого комитет решил создать новую стандартную команду для компилятора языка С — с89. Если эта команда представлена, она всегда использует одни и те же опции независимо от машины.

В системах Linux, которые на деле пытаются следовать стандартам, можно обнаружить, что все или некоторые из команд с89, cc и gcc ссылаются на системный компилятор языка С, обычно компилятор GNU С или gcc. В системах UNIX компилятор языка С почти всегда называется cc.

В этой книге мы используем gcc, поскольку он поставляется в дистрибутивах Linux и потому что он поддерживает для языка С синтаксис стандарта ANSI. Если когда-нибудь вы обнаружите, что в вашей системе нет gcc, мы советуем получить его и установить. Найти его вы можете по адресу http://www.gnu.org. Всюду, где мы используем в книге команду gcc, просто заменяйте ее подходящей командой вашей системы.

Упражнение 1.1. Ваша первая Linux-программа на языке C

В этом примере вы начнете разработку в ОС Linux с помощью языка С, написав, откомпилировав и выполнив свою первую Linux-программу. Ею, кстати, может стать самая известная из всех программ для начинающих — программа, выводящая сообщение "Hello World" ("Привет, мир").

1. Далее приводится текст файла hello.c:

#include <stdio.h>

#include <stdlib.h>

int main() {

 printf("Hello World\n");

 exit(0);

}

2. Теперь откомпилируйте, скомпонуйте и выполните вашу программу.

$ gcc -о hello.c $ ./hello

Hello World

Как это работает

Вы запустили компилятор GNU С (в Linux, вероятнее всего, он будет доступен и как cc), который оттранслировал исходный код на языке С в исполняемый файл, названный hello. Вы выполнили программу, и она вывела на экран приветствие. Это наипростейший из существующих примеров, но если вы смогли с помощью вашей системы добраться до этого места, то сможете откомпилировать и выполнить и остальные примеры из книги. Если же программа не сработала, убедитесь в том, что в вашей системе установлен компилятор языка С. Например, во многих дистрибутивах Linux есть установочная опция, названная Software Development (Разработка ПО) (или что-то похожее), которую следует выбрать для установки необходимых пакетов.

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

Если вы забыли опцию -o name, которая указывает компилятору, куда поместить исполняемый файл, компилятор поместит его в файл с именем a.out (что означает ассемблерный вывод). Не забудьте поискать файл с именем a.out, если вы уверены, что скомпилировали программу, а найти ее не можете! Когда ОС UNIX только появилась, пользователи, хотевшие играть в ней в игры, часто запускали их как файл с именем a.out, чтобы не быть пойманными системным администратором, и некоторые установки ОС UNIX традиционно удаляют каждый вечер все файлы с именем a.out.

Маршрутная карта системы разработки

Разработчику ОС Linux важно знать кое-что о том, где размещаются средства и ресурсы разработки. В следующих разделах дан краткий обзор некоторых важных каталогов и файлов.

Приложения

Приложения обычно хранятся в отведенных для них каталогах. Приложения, предоставляемые системой для общего использования, включая средства разработки программ, находятся в каталоге /usr/bin. Приложения, добавленные системными администраторами для конкретного хост-компьютера или локальной сети, часто хранятся в каталоге /usr/local/bin или /opt.

Администраторы предпочитают /opt и /usr/local, потому что они хранят предоставляемые конкретными поставщиками файлы и свежие дополнения отдельно от приложений, предоставляемых системой. Подобная организация хранения файлов может помочь, когда придет время обновлять операционную систему, т.к. в этом случае потребуется сберечь только каталоги /opt и /usr/local. Мы рекомендуем компилировать в ветви иерархии /usr/local только системные приложения общего назначения, предназначенные для запуска и доступа к требуемым файлам. Для разрабатываемых программ и личных приложений лучше всего применять папку в вашем исходном каталоге.

Дополнительные средства и системы программирования могут иметь собственные структуры каталогов и каталоги программ. Важнейшая среди них — графическая оболочка X Window System, которая обычно устанавливается в каталог /usr/X11 или каталог /usr/bin/X11. В дистрибутивах Linux, как правило, применяется версия X.Org Foundation графической оболочки X Window System, базирующаяся на модификации Revision 7 (X11R7). В других UNIX-подобных системах могут быть выбраны иные версии X Window System, устанавливаемые в другие каталоги, например, каталог /usr/openwin для оболочки Open Windows компании Sun в системе Solaris.

Программа системного драйвера компилятора GNU, gcc (которую вы использовали в предыдущем упражнении) обычно помещается в каталог usr/bin или usr/local/bin, но она будет запускать различные поддерживающие компиляцию приложения из других каталогов. Эти каталоги задаются во время компиляции самого компилятора и зависят от типа хост-компьютера. В системах Linux это может быть зависящий от конкретной версии подкаталог /usr/lib/gcc/. На одной из машин одного из авторов во время написания книги это был подкаталог /usr/lib/gcc/i586-suse-linux/4.1.3. В нем хранятся отдельные проходы компилятора GNU C/C++ и специфические заголовочные файлы GNU.

Заголовочные файлы

В процессе программирования на языке С и других языках вам потребуются заголовочные файлы или файлы заголовков для включения определений констант и объявлений вызовов системных и библиотечных функций. В случае языка С эти файлы почти всегда находятся в каталоге /usr/include и его подкаталогах. Заголовочные файлы, зависящие от конкретного воплощения запущенной вами ОС Linux, вы, как правило, найдете в каталогах /usr/include/sys и /usr/include/linux.

У других систем программирования тоже есть заголовочные файлы, хранящиеся в каталогах, которые автоматически находятся соответствующим компилятором. Примерами могут служить каталоги /usr/include/X11 для графической оболочки X Window System и /usr/include/c++ для языка GNU С++.

Вы можете использовать заголовочные файлы из подкаталогов или нестандартных мест хранения, указав флаг -I (для include) в строке вызова компилятора языка С. Например, команда

$ gcc -I/usr/openwin/include fred.c

заставит искать заголовочные файлы, использованные в программе fred.c, в стандартных каталогах и в каталоге /usr/openwin/include. Для получения дополнительных сведений обратитесь к руководству компилятора С (man gcc).

Искать заголовочные файлы с конкретными определениями и прототипами конкретных функций часто удобно с помощью команды grep. Предположим, вам нужно знать имя из директив #define, используемое для возврата из программы статуса завершения. Просто замените каталог на /usr/include и примените grep для поиска предполагаемой части имени следующим образом:

$ grep EXIT_ *.h

...

stdlib.h#define EXIT_FAILURE 1 /*Failing exit status. */

stdlib.h#define EXIT_SUCCESS 0 /*Successful exit status. */

...

$

В этом случае команда grep ищет в каталоге все файлы с именами, заканчивающимися на .h, со строкой EXIT_. В данном примере она нашла (среди прочих) нужное вам определение в файле stdlib.h.

Библиотечные файлы

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

Стандартные системные библиотеки обычно хранятся в каталогах /lib и /usr/lib. Компилятору языка С (или, точнее, компоновщику) необходимо сообщить, в каких библиотеках искать, поскольку по умолчанию он ищет только в стандартной библиотеке С. Это пережиток, пришедший к нам из того времени, когда компьютеры были медленными и циклы ЦПУ были дороги. Недостаточно поместить библиотеку в стандартный каталог и ждать, что компилятор найдет ее; библиотеки должны следовать очень специфическим правилам именования и быть упомянуты в командной строке.

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

□ а — для традиционных статических библиотек;

□ .so — для совместно используемых библиотек (см. далее).

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

$ gcc -о fred fred.c /usr/lib/libm.a

сообщает компилятору о необходимости компилировать файл fred.c и искать разрешения ссылок на функции в библиотеке математических функций в дополнение к стандартной библиотеке С. Аналогичного результата можно добиться с помощью следующей команды:

$ gcc -о fred fred.c -lm

-lm (без пробела между символами l и m) — это сокращенное обозначение (сокращенные формы очень ценятся в UNIX-кругах) библиотеки с именем libm.a, хранящейся в одном из стандартных библиотечных каталогов (в данном случае /usr/lib). Дополнительное преимущество обозначения -lm в том, что компилятор автоматически выберет совместно используемую библиотеку, если она существует.

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

$ gcc -о x11fred -L/usr/openwin/lib x11fred.c -lX11

будет компилировать и компоновать программу x11fred, используя версию библиотеки libX11, найденную в каталоге /usr/openwin/lib.

Статические библиотеки

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

Статические библиотеки, также называемые архивами, в соответствии с принятыми соглашениями имеют окончание .а. Например, lib/libc.а и /usr/lib/libX11 для библиотек С и X11 соответственно.

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

Упражнение 1.2. Статические библиотеки

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

1. Сначала создайте отдельные исходные файлы (как не удивительно, названные fred.c и bill.c) для каждой функции.

Далее приведен первый из них:

#include <stdio.h>

void fred(int arg) {

 printf("fred: you passed %d\n", arg);

}

А это второй:

#include <stdio.h>

void bill(char *arg) {

 printf("bill: you passed %s\n", arg);

}

2. Вы можете отдельно откомпилировать эти функции и создать объектные файлы, готовые к включению в библиотеку. Для этого запустите компилятор С с опцией , которая помешает компилятору создать законченную программу. Попытка создать законченную программу окажется безуспешной, т.к. вы не определили функцию с именем main.

$ gcc -с bill.с fred.c

$ ls *.o

bill.о fred.о

3. Теперь напишите программу, вызывающую функцию bill. Прежде всего, хорошо бы создать заголовочный файл для вашей библиотеки. В нем будут объявлены функции из вашей библиотеки, и он будет включаться во все приложения, которые захотят применить вашу библиотеку. В файлы fred.c и bill.c тоже хорошо бы включить заголовочный файл, чтобы помочь компилятору обнаружить любые ошибки.

/*

 Это файл lib.h. В кем объявлены пользовательские функции fred and bill

*/[1]

void bill(char *);

void fred(int);

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

#include <stdlib.h>

#include "lib.h"

int main() {

 bill("Hello World");

 exit(0);

}

5. Теперь можно откомпилировать и протестировать программу. Для этого задайте компилятору явно объектные файлы и попросите его откомпилировать ваш файл и связать его с ранее откомпилированным объектным модулем bill.o.

$ gcc -с program.с

$ gcc -о program program.о bill.о

$ ./program

bill: we passed Hello World

$

6. Для создания архива и включения в него ваших объектных файлов используйте программу ar. Программа называется ar, поскольку она создает архивы или коллекции отдельных файлов, помещая их все в один большой файл. Имейте в виду, что программу ar можно применять для создания архивов из файлов любого типа. (Как многие утилиты UNIX, ar — универсальное средство.)

$ ar crv libfоо.a bill.о fred.о

а - bill.о а - fred.о

7. Библиотека создана, и в нее добавлены два объектных файла. Для того чтобы успешно применять библиотеку в некоторых системах, в особенности в производных от Berkeley UNIX, требуется создать для библиотеки индекс содержимого архива или список вложенных в библиотеку функций и переменных (table of contents). Сделайте это с помощью команды ranlib. В ОС Linux при использовании программных средств разработки GNU этот шаг не является необходимым (но и не приносит вреда).

$ ranlib libfoo.a

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

$ gcc -о program program.о libfоо.а

$ ./program

bill: we passed Hello world

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

$ gcc -о program .program.о -L. -lfoo

Опция -L заставляет компилятор искать библиотеки в текущем каталоге (.). Опция -lfoo сообщает компилятору, что нужно использовать библиотеку с именем libfoo.a (или совместно используемую библиотеку libfoo.so, если она есть). Для того чтобы посмотреть, какие функции включены в объектный файл, библиотеку или исполняемую программу, можно применить команду nm. Если вы взглянете на файлы program и libfoo.a, то увидите, что библиотека содержит обе функции: fred и bill, а файл program — только функцию bill. Когда создается программа, в нее включаются из библиотеки только те функции, которые ей действительно нужны. Вставка заголовочного файла, содержащего объявления всех функций библиотеки, не вызывает включения в конечную программу целиком всей библиотеки.

Если вы знакомы с разработкой программ в ОС Windows, то поймете, что в ОС UNIX существует ряд прямых аналогий, перечисленных в табл. 1.1.

Таблица 1.1

Элемент UNIX Windows
Объектный модуль func.o FUNC.OBJ
Статическая библиотека lib.a LIB.LIB
Программа program PROGRAM.EXE

Совместно используемые библиотеки

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

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

Совместно используемые библиотеки хранятся в тех же каталогах, что и статические, но у имен файлов совместно используемых библиотек другой суффикс. В типовой системе Linux имя совместно используемой версии стандартной библиотеки математических функций — /lib/libm.so.

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

В этом случае система предоставляет возможность многим приложениям одновременно использовать единственную копию совместно используемой библиотеки и хранить ее на диске в единственном экземпляре. Дополнительным преимуществом служит возможность обновления совместно используемой библиотеки независимо от базирующихся на ней приложений. Применяются символические ссылки из файла /lib/libm.so на текущую версию библиотеки (/lib/libm.so.N, где N — основной номер версии — 6 во время написания книги). Когда ОС Linux запускает приложение, она учитывает номер версии библиотеки, требующийся приложению, чтобы не дать ведущим новым версиям библиотеки испортить более старые приложения.

Примечание

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

В системах Linux программа (динамический загрузчик), отвечающая за загрузку совместно используемых библиотек и разрешение ссылок на функции в клиентских программах, называется ld.so и может присутствовать в системе как ld-linux.so.2, или li-lsb.so.2, или li-lsb.so.3. Дополнительные каталоги поиска совместно используемых библиотек настраиваются в файле /etc/ld.so.conf, который после внесения изменений (например, если добавляются совместно используемые библиотеки X11 при установке графической оболочки X Window System) следует обработать командой ldconfig.

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

$ ldd program

  linux-gate.so.1 => (0xffffe000)

  libc.so.6 => /lib/libc.so.6 (0xb7db4000)

  /lib/ld-linux.so.2 (0xb7efc000)

В этом случае вы видите стандартную библиотеку С (libc) как совместно используемую (.so). Программе требуется основная версия 6. В других системах UNIX принимаются аналогичные меры для получения доступа к совместно используемым библиотекам. Подробную информацию можно найти в системной документации.

Во многом совместно используемые библиотеки аналогичны динамически подключаемым библиотекам в ОС Windows. Библиотеки с расширением .so соответствуют файлам с расширением dll и требуются во время выполнения, а библиотеки с расширением .а аналогичны файлам с расширением lib, которые включаются в исполняемые программы.

Получение справки

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

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

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

Упражнение 1.3. Справочные руководства и система info

Давайте познакомимся с документацией для компилятора GNU С (gcc).

1. Сначала посмотрим на справочное руководство.

$ man gcc

GCC(1)                GNU                GCC(1)

NAME

       gcc — GNU project С and С++ compiler

SYNOPSIS

       gcc [-с|-S|-E] [-std=standard]

           [-g] [-pg] [-Olevel]

           [-Wwarn...] [-pedantic]

           [-Idir...] [-Ldir...]

           [-Dmacro[=defn]...] [-Umacro]

           [-foption...] [-mmachine-option...]

           [-о outfile] infile...

       Only the most useful options are listed here; see below

       for the remainder. g++ accepts mostly the same options as

       gcc.

DESCRIPTION

       When you invoke GCC, it normally does preprocessing, com-

       pilation, assembly and linking. The "overall options"

       allow you to stop this process at an intermediate stage.

       For example, the -c option says not to run the linker.

       Then the output consists of object files output by the assembler.

       Other options are passed on to one stage of processing.

       Some options control the preprocessor and others the com-

       piler itself. Yet other options control the assembler and

       linker; most of these are not documented here, since we

       rarely need to use any of them.

...

Если хотите, можно прочесть об опциях, поддерживаемых транслятором. В этом случае справочное руководство очень длинное, хотя содержит лишь малую часть полной документации по компилятору GNU С (и С++).

При чтении справочных страниц можно использовать клавишу <Пробел> для перехода к следующей странице, клавишу <Enter> (или клавишу <Return>, если на вашей клавиатуре применяется эта клавиша вместо <Enter>) для перехода к следующей строке и клавишу <q> для полного выхода из программы.

2. Для получения более подробной информации о компиляторе GNU С можно попробовать применить команду info.

$ info gcc

File: gcc.info. Node: Top, Next: G++ and GCC, Up: (DIR)

Introduction

************

   This manual documents how to use the GNU compilers, as well as their

features and incompatibilities, and how to report bugs. It corresponds to

GCC version 4.1.3. The internals of the GNU compilers, including how to port

them to new targets and some information about how to write front ends for

new languages, are documented in a separate manual.

*Note Introduction: (gccint)Top.

*Menu:

* G++ and GCC:: You can compile С or С++ Applications.

* Standards:: Language standards supported by GCC,

* Invoking GCC:: Command options supported by `gcc'.

* С Implementation:: How GCC implements theISO С specification.

* С Extensions:: GNU extensions to the С language family.

* С++ Extensions:: GNU extensions to the С++ language.

* Objective-C:: GNU Objective-C runtime features.

* Compatibility:: Binary Compatibility

--zz-Info: (gcc.info.gz)Top, 39 lines --Top--------------------------

Welcome to Info version 4.8. Type ? for help, m for menu item.

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

У системы info есть собственная справка, конечно, в формате страниц info. Если нажать комбинацию клавиш <Ctrl>+<H>, можно познакомиться со справочным руководством, включающим средства обучения пользованию системой info. Программа info входит в состав многих дистрибутивов Linux и может устанавливаться в других ОС UNIX.

Резюме 

В этой вводной главе мы познакомились с программированием в ОС Linux и другими компонентами ОС Linux, общими с патентованными системами UNIX. Мы отметили огромное разнообразие систем программирования, доступных UNIX-разработчикам. Мы также представили простые программу и библиотеку, чтобы показать базовые средства языка С и сравнить их с эквивалентными средствами в ОС Windows.

Глава 2

Программирование средствами командной оболочки

Начав книгу с программирования в ОС Linux на языке С, теперь мы сделаем отступление и остановимся на написании программ в командной оболочке. Почему? ОС Linux не относится к системам, у которых интерфейс командной строки — запоздалое детище графического интерфейса. У систем UNIX, прообраза Linux, первоначально вообще не было графического интерфейса; все выполнялось из командной строки. Поэтому оболочка командной строки UNIX все время развивалась и превратилась в очень мощный инструмент. Эти свойства перекочевали и в Linux, и некоторые самые серьезные задачи вы можете выполнить наиболее легким способом именно из командной оболочки. Поскольку она так важна для ОС Linux и столь полезна для автоматизации простых задач, программирование средствами командной оболочки рассматривается прежде всего.

В этой главе мы познакомим вас с синтаксисом, структурами и командами, доступными при программировании в командной оболочке, как правило, используя интерактивные (основанные на экранах) примеры. Они помогут продемонстрировать функциональные возможности командной оболочки и собственные действия. Мы также бросим беглый взгляд на пару особенно полезных утилит режима командной строки, часто вызываемых из командной оболочки: grep и find. Рассматривая утилиту grep, мы познакомимся с основными положениями, касающимися регулярных выражений, которые появляются в утилитах ОС Linux и языках программирования, таких как Perl, Ruby и PHP. В конце главы вы узнаете, как писать настоящие сценарии, которые будут перепрограммироваться и расширяться на языке С на протяжении всей книги. В этой главе рассматриваются следующие темы:

□ что такое командная оболочка;

□ теоретические основы;

□ тонкости синтаксиса: переменные, условия и управление программой;

□ списки;

□ функции;

□ команды и их выполнение;

□ встроенные (here) документы;

□ отладка;

□ утилита grep и регулярные выражения;

□ утилита find.

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

Почему программа в командной оболочке?

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

Хотя внешне командная оболочка очень похожа на режим командной строки в ОС Windows, она гораздо мощнее и способна выполнять самостоятельно очень сложные программы. Вы можете не только выполнять команды и вызывать утилиты ОС Linux; но и разрабатывать их. Командная оболочка выполняет программы оболочки, часто называемые сценариями или скриптами, которые интерпретируются во время выполнения. Такой подход облегчает отладку, потому что вы легко можете выполнять программу построчно и не тратить время на перекомпиляцию. Но для задач, которым важно время выполнения или необходимо интенсивное использование процессора, командная оболочка оказывается неподходящей средой.

Немного теории

Вот мы и добрались до теоретических основ UNIX и, конечно, Linux. ОС UNIX основана на интенсивном многократном применении кода и зависит от него. Вы разработали маленькую простую утилиту, и пользователи применяют ее как одну из ссылок в строке, формирующей команду. Одно из наслаждений, доставляемых ОС Linux, — разнообразие имеющихся отличных средств. Примером может служить следующая команда:

$ ls -al | more

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

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

man bash | col -b | lpr

Более того, благодаря автоматической обработке типов файлов пользователям этих утилит обычно не нужно знать, на каком языке данные программы написаны. Если необходимо ускорить выполнение утилиты, как правило, ее сначала моделируют в командной оболочке и затем, когда работоспособность утилиты проверена, реализуют ее на языке С или С++, Perl, Python или каком-либо другом, обеспечивающем более быстрое выполнение. В противном случае, если в командной оболочке утилита действует адекватно, вы вполне можете оставить ее в покое.

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

Примечание

Для удовлетворения вашего любопытства в вашу ОС Linux уже загружены многочисленные примеры сценариев, включая инсталляторы пакетов, .xinitrc и startx, и сценарии в каталоге /etc/rc.d, предназначенные для настройки системы в процессе загрузки.

Что такое командная оболочка?

Прежде чем переходить к обсуждению того, как программа использует оболочку, давайте рассмотрим, как функционирует оболочка и какие оболочки есть в Linux-подобных системах. Командная оболочка — это программа, которая действует как интерфейс между вами и ОС Linux, позволяя вам вводить команды, которые должна выполнить операционная система. В этом смысле она похожа на командную строку в ОС Windows, но, как уже упоминалось, командные оболочки Linux гораздо мощнее. Например, ввод и вывод можно перенаправить с помощью символов < и >, передавать данные между двумя одновременно выполняющимися программами с помощью символа |, а перехватывать вывод подпроцесса с помощью конструкции $(...). В ОС Linux вполне может сосуществовать несколько установленных командных оболочек, и разные пользователи могут выбрать ту, которая им больше нравится. На рис. 2.1 показано, как командная оболочка (на самом деле, две командные оболочки: bash и csh) и другие программы располагаются вокруг ядра Linux.

Рис. 2.1

Поскольку ОС Linux — модульная система, вы можете вставить и применять одну из множества различных стандартных командных оболочек, хотя большинство из них — потомки первоначальной оболочки Bourne. В Linux стандартная командная оболочка, всегда устанавливаемая как /bin/sh и входящая в комплект средств проекта GNU, называется bash (GNU Bourne-Again SHell). Именно ее мы будем применять, т. к. это отличная командная оболочка, всегда устанавливаемая в системах Linux, со свободно распространяемым программным кодом и переносимая почти на все варианты UNIX-систем. В данной главе используется оболочка bash версии 3, и в большинстве случаев применяются ее функциональные возможности, общие для всех командных оболочек, удовлетворяющих требованиям стандарта POSIX. Мы полагаем, что командная оболочка, установленная как /bin/sh и для вашей учетной записи, считается командной оболочкой по умолчанию. В большинстве дистрибутивов Linux программа /bin/sh, командная оболочка по умолчанию, — это ссылка на программу /bin/bash.

Вы можете определить используемую в вашей системе версию bash с помощью следующей команды:

$ /bin/bash --version

GNU bash, version 3.2.9(1)-release (i686-pc-linux-gnu)

Copyright (C) 2005 Free Software Foundation, Inc.

Примечание

Для перехода на другую командную оболочку, если в вашей системе по умолчанию установлена не bash, просто выполните программу нужной вам командной оболочки (т.е. /bin/bash) для запуска новой оболочки и смены приглашения в командной строке. Если вы используете ОС UNIX, и командная оболочка bash не установлена, вы можете бесплатно загрузить ее с Web-сайта www.gnu.org. Исходный код обладает высокой степенью переносимости, и велика вероятность, что он откомпилируется в вашей версии UNIX прямо в готовую к использованию программу.

Когда создаются учетные записи пользователей ОС Linux, вы можете задать командную оболочку, которой они будут пользоваться, в момент создания учетной записи пользователя или позже, откорректировав ее параметры. На рис. 2.2 показан выбор командной оболочки для пользователя дистрибутива Fedora.

Рис. 2.2

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

Таблица 2.1

Название командной оболочки Краткие исторические сведения
sh (Bourne) Первоначальная оболочка в ранних версиях ОС UNIX
csh, tcsh, zsh Командная оболочка C-shell (и ее производные), первоначально созданная Биллом Джойем (Bill Joy) для систем Berkeley UNIX. C-shell, возможно, третья по популярности командная оболочка после оболочек bash и Korn
ksh, pdksh Командная оболочка Korn и ее безлицензионный родственник. Написанная Дэвидом Корном (David Korn) эта оболочка применяется по умолчанию во многих коммерческих версиях UNIX
bash Основная командная оболочка ОС Linux из проекта GNU или Bourne Again SHell со свободно распространяемым программным кодом. Если в настоящий момент она не выполняется в вашей системе UNIX, вероятно, есть вариант оболочки, перенесенный на вашу систему. У bash много сходств с оболочкой Korn

За исключением оболочки C-shell и небольшого числа ее производных все перечисленные оболочки очень похожи и очень близки к оболочке, определенной в спецификациях Х/Оpen 4.2 и POSIX 1003.2. В спецификации POSIX 1003.2 задан минимум, необходимый для создания командной оболочки, а в спецификации Х/Open представлена более дружественная и мощная оболочка.

Каналы и перенаправление

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

Перенаправление вывода

Возможно, вы уже знакомы с некоторыми видами перенаправления, например, таким как:

$ ls -l > lsoutput.txt

сохраняющим вывод команды ls в файле с именем lsoutput.txt.

Однако перенаправление позволяет сделать гораздо больше, чем демонстрирует этот простой пример. В главе 3 вы узнаете больше о дескрипторах стандартных файлов, а сейчас вам нужно знать только то, что дескриптор файла 0 соответствует стандартному вводу программы, дескриптор файла 1 — стандартному выводу, а дескриптор файла 2 — стандартному потоку ошибок. Каждый из этих файлов можно перенаправлять независимо друг от друга. На самом деле можно перенаправлять и другие дескрипторы файлов, но, как правило, нет нужды перенаправлять любые другие дескрипторы, кроме стандартных: 0, 1 и 2.

В предыдущем примере стандартный вывод перенаправлен в файл с помощью оператора >. По умолчанию, если файл с заданным именем уже есть, он будет перезаписан. Если вы хотите изменить поведение по умолчанию, можно применить команду set -о noclobber (или set -С), которая устанавливает опцию noclobber, чтобы помешать перезаписи при перенаправлении. Отменить эту опцию можно с помощью команды set +о noclobber. Позже в этой главе будут приведены другие опции команды set.

Для дозаписи в конец файла используйте оператор >>. Например, команда

ps >> lsoutput.txt

добавит вывод команды ps в конец заданного файла.

Для перенаправления стандартного потока ошибок перед оператором > вставьте номер дескриптора файла, который хотите перенаправить. Поскольку у стандартного потока ошибок дескриптор файла 2, укажите оператор 2>. Часто бывает полезно скрывать стандартный поток ошибок, запрещая вывод его на экран.

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

Команда

$ kill -HUP 1234 >killout. txt 2>killer.txt

поместит вывод и информацию об ошибке в разные файлы.

Если вы предпочитаете собрать оба набора выводимых данных в одном файле, можно применить оператор >& для соединения двух выводных потоков. Таким образом, команда

$ kill -1 1234 >killerr.txt 2>&1

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

Поскольку обнаружить результат выполнения команды kill можно с помощью кода завершения (который будет подробно обсуждаться далее в этой главе), часто вам не потребуется сохранять какой бы то ни было стандартный вывод или стандартный поток ошибок. Для того чтобы полностью отбросить любой вывод, вы можете использовать универсальную "мусорную корзину" Linux, /dev/null, следующим образом:

$ kill -l 1234 >/dev/null 2>&1

Перенаправление ввода

Также как вывод вы можете перенаправить ввод. Например,

$ more < killout.txt

Понятно, что это тривиальнейший пример для ОС Linux; команда more в системе Linux в отличие от своего эквивалента командной строки в ОС Windows с радостью принимает имена файлов в качестве параметров.

Каналы 

Вы можете соединять процессы с помощью оператора канала (|). В ОС Linux, в отличие от MS-DOS, процессы, соединенные каналами, могут выполняться одновременно и автоматически переупорядочиваться в соответствии с потоками данных между ними. Как пример, можно применить команду sort для сортировки вывода команды ps.

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

$ ps > psout.txt

$ sort psout.txt > pssort.out

Соединение процессов каналом даст более элегантное решение:

$ ps | sort > pssort.out

Поскольку вы, вероятно, захотите увидеть на экране вывод, разделенный на страницы, можно подсоединить третий процесс, more, и все это в одной командной строке:

$ ps | sort | more

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

$ ps -хо соmm | sort | uniq | grep -v sh | more

В ней берется вывод команды ps, сортируется в алфавитном порядке, из него извлекаются процессы с помощью команды uniq, применяется утилита grep -v sh для удаления процесса с именем sh и в завершение полученный список постранично выводится на экран.

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

cat mydata.txt | sort | uniq > mydata.txt

то в результате получите пустой файл, т.к. вы перезапишете файл mydata.txt, прежде чем прочтете его.

Командная оболочка как язык программирования

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

Интерактивные программы

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

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

$ for file in *

> do

> if grep -l POSIX $file

> then

> more $file

> fi

> done

posix

This is a file with POSIX in it - treat it well

$

Обратите внимание на то, как меняется знак $, стандартная подсказка или приглашение командной оболочки, на символ >, когда оболочка ожидает очередной ввод. Вы можете продолжить набор, дав оболочке понять, когда закончите, и сценарий немедленно выполнится.

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

Командная оболочка также обрабатывает групповые символы или метасимволы (часто называемые знаками подстановки). Вы почти наверняка знаете о применении символа * как знака подстановки, соответствующего строке символов. Но вы можете не знать о существовании односимвольного знака подстановки, ?, а конструкция [set] позволяет проверить любое количество одиночных символов, [^set] — применяет логическую операцию "НЕ" к множеству, т.е. включает все, кроме того, что вы задали. Подстановочный шаблон из фигурных скобок {} (доступен в некоторых командных оболочках, включая bash) позволяет формировать множество из произвольных строк, которое командная оболочка раскроет. Например, команда

$ ls my_{finger, toe}s

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

Опытные пользователи ОС Linux, вероятно, выполнят эту операцию более эффективным способом, возможно, с помощью следующей команды:

$ more `grep -l POSIX *`

или синонимической конструкции

$ more $(grep -l POSIX *)

В дополнение команда

$ grep -l POSIX * | more

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

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

Создание сценария

С помощью любого текстового редактора необходимо создать файл, содержащий команды. Создайте файл с именем first с таким содержимым:

#!/bin/sh

# first

# Этот файл просматривает все файлы в текущем каталоге

# для поиска строки POSIX, а затем выводит имена

# найденных файлов в стандартный вывод.

for file in *

do

 if grep -q POSIX $file

 then

  echo $file

 fi

done

exit 0

Комментарий начинается со знака # и продолжается до конца строки. Принято знак # ставить в первой символьной позиции строки. Сделав такое общее заявление, далее отметим, что первая строка #!/bin/sh — это особая форма комментария; символы #! сообщают системе о том, что следующий за ними аргумент — программа, применяемая для выполнения данного файла. В данном случае программа /bin/sh — командная оболочка, применяемая по умолчанию. 

Примечание

Обратите внимание на абсолютный путь, заданный в комментарии. Принято сохранять его длиной не более 32 символов для обратной совместимости, поскольку некоторые старые версии ОС UNIX могут использовать только такое ограниченное количество символов в комментарии #!, хотя у ОС Linux обычно нет подобного ограничения.

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

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

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

В сценарии не используются никакие расширения и суффиксы имен файлов; ОС Linux и UNIX, как правило, редко применяют при именовании файлов расширения для указания типа файла. Вы могли бы использовать расширение sh или любое другое, командную оболочку это не волнует. У большинства предустановленных сценариев нет никакого расширения в именах файлов и лучший способ проверить, сценарий это или нет применить команду file, например, file first или file /bin/bash. Пользуйтесь любыми правилами, принятыми в вашей организации или удобными для вас.

Превращение сценария в исполняемый файл

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

$ /bin/sh first

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

$ chmod +х first 

Примечание

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

После этого вы можете выполнять файл с помощью команды

$ first

При этом может появиться сообщение об ошибке, говорящее о том, что команда не найдена. Почти наверняка причина в том, что в переменной PATH не задан текущий каталог для поиска выполняемых команд. Исправить это можно либо введя с клавиатуры в командной строке PATH=$PATH:., либо добавив данную команду в конец файла .bash_profile. Затем выйдите из системы и зарегистрируйтесь снова. В противном случае введите ./first в каталог, содержащий сценарий, чтобы задать командной оболочке полный относительный путь к файлу.

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

Примечание

Не следует вносить подобные изменения в переменную PATH для суперпользователя, как правило, с именем root. Это лазейка в системе безопасности, т.к. системного администратора, зарегистрировавшегося как root, обманным путём могут заставить запустить фиктивную версию стандартной команды. Один из авторов однажды разрешил сделать это — конечно только для того, чтобы поставить перед системным администратором вопрос о безопасности! В случае обычных учетных записей включение текущего каталога в полный путь сопряжено с очень небольшим риском, поэтому, если вам это нужно, примите за правило добавление комбинации символов ./ перед всеми командами, находящимися в локальном каталоге.

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

# ср first /usr/local/bin

# chown root /usr/local/bin/first

# chgrp root /usr/local/bin/first

# chmod 755 /usr/local/bin/first

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

Если захотите, можно применить более длинную, но более понятную форму команды chmod:

# chmod u=rwx, go=rx /usr/local/bin/first

Более подробную информацию можно найти в справочном руководстве команды chmod.

Примечание

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

Синтаксис командной оболочки

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

□ переменные: строки, числа, переменные окружения и параметры;

□ условия: булевы или логические выражения (Booleans);

□ управление выполнением программы: if, elif, for, while, until, case;

□ списки;

□ функции;

□ команды, встроенные в командную оболочку;

□ получение результата выполнения команды;

□ встроенные (here) документы.

Переменные

В командной оболочке переменные перед применением обычно не объявляются. Вместо этого вы создаете их, просто используя (например, когда присваиваете им начальное значение). По умолчанию все переменные считаются строками и хранятся как строки, даже когда им присваиваются числовые значения. Командная оболочка и некоторые утилиты преобразуют строки, содержащие числа, в числовые значения, когда нужно их обработать должным образом. Linux — система, чувствительная к регистру символов, поэтому командная оболочка считает foo и Foo двумя разными переменными, отличающимися от третьей переменной FOO.

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

Вы можете увидеть это в действии, если в командной строке будете задавать и проверять разные значения переменной salutation:

$ salutation=Hello

$ echo $salutation

Hello

$ salutation="Yes Dear"

$ echo $salutation

Yes Dear

$ salutation=7+5

$ echo $salutation

7+5

Примечание

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

Вы можете присвоить переменной пользовательский ввод с помощью команды read. Она принимает один параметр — имя переменной, в которую будут считываться данные, и затем ждет, пока пользователь введет какой-либо текст. Команда read обычно завершается после нажатия пользователем клавиши <Enter>. При чтении переменной с терминала, как правило, заключать ее значения в кавычки не требуется:

$ read salutation

Wie geht's?

$ echo $salutation

Wie geht's?

Заключение в кавычки

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

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

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

Выполним упражнение 2.1.

Упражнение 2.1. Игра с переменными

В этом упражнении показано, как кавычки влияют на вывод переменной:

#!/bin/sh

myvar="Hi there"

echo $myvar

echo "$myvar"

echo '$myvar'

echo \$myvar

echo Enter some text

read myvar

echo '$myvar' now equals $myvar

exit 0

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

$ ./variable

Hi there

Hi there

$myvar

$myvar

Enter some text

Hello World

$myvar now equals Hello World

Как это работает

Создается переменная myvar, и ей присваивается строка Hi there. Содержимое переменной выводится на экран с помощью команды echo, демонстрирующей, как символ $ раскрывает содержимое переменной. Вы видите, что применение двойных кавычек не влияет на раскрытие содержимого переменной, а одинарные кавычки и обратный слэш влияют. Вы также применяете команду read для получения строки от пользователя.

Переменные окружения

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

Таблица 2.2

Переменная окружения Описание
$НОМЕ Исходный каталог текущего пользователя
$PATH Разделенный двоеточиями список каталогов для поиска команд
$PS1 Подсказка или приглашение командной строки. Часто знак $, но в оболочке bash можно применять и более сложные варианты. Например, строка [\u@\h \w]$ — популярный стандарт, сообщающий в подсказке пользователя имя компьютера и текущий каталог, а также знак $
$PS2 Дополнительная подсказка или приглашение, применяемое как приглашение для дополнительного ввода; обычно знак >
$IFS Разделитель полей ввода. Список символов, применяемых для разделения слов при чтении оболочкой ввода, как правило, пробел, знак табуляции и символ перехода на новую строку
$0 Имя сценария командной оболочки
$# Количество передаваемых параметров
$$ ID (идентификатор) процесса сценария оболочки, часто применяемый внутри сценария для генерации уникальных имен временных файлов; например, /tmp/tmpfile_$$

Примечание

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

Переменные-параметры

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

Переменные-параметры перечислены в табл. 2.3.

Таблица 2.3

Переменная-параметр Описание
$1, $2, ... Параметры, передаваемые сценарию
$* Список всех параметров в единственной переменной, разделенных первым символом из переменной окружения IFS. Если IFS корректируется, способ разделения командной строки на параметры в переменной $* изменяется
$@ Едва различимая вариация $*; не использует переменную окружения IFS, поэтому параметры не сольются, даже если переменная IFS пуста

Легче всего увидеть разницу между переменными-параметрами $* и $@, опробовав их.

$ IFS=''

$ set foo bar bam

$ echo "$@"

foo bar bam

$ echo "$*"

foobarbam

$ unset IFS

$ echo "$*"

foo bar bam

Как видите, заключенная в двойные кавычки переменная-параметр $@ представляет позиционные параметры как отдельные поля, независимо от значения переменной окружения IFS. Как правило, если вы хотите получить доступ к параметрам, лучше использовать переменную-параметр.

Помимо вывода на экран содержимого переменных с помощью команды echo, вы также можете прочитать его командой read (упражнение 2.2).

Упражнение 2.2. Манипулирование параметрами и переменными окружения

В приведенном далее сценарии показано несколько простых манипуляций переменными. После ввода сценария и записи его в файл try_var не забудьте превратить его в исполняемый файл с помощью команды chmod +х try_var.

#!/bin/sh

salutation="Hello"

echo $salutation

echo "The program $0 is now running"

echo "The second parameter was $2"

echo "The first parameter was $1"

echo "The parameter list was

echo "The user's home directory is $HOME"

echo "Please enter a new greeting"

read salutation

echo $salutation

echo "The script is now complete"

exit 0

Если вы выполните этот сценарий, то получите следующий вывод:

$ ./try_var foo bar baz

Hello

The program ./try_var is now running

The second parameter was bar

The first parameter was foo

The parameter list was foo bar baz

The user's home directory is /home/rick

Please enter a new greeting

Sire

Sire

The script is now complete $

Как это работает

Сценарий создает переменную salutation, выводит на экран ее содержимое и затем показывает, что уже сформированы и имеют соответствующие значения различные переменные-параметры и переменная окружения $НОМЕ.

Далее в этой главе мы рассмотрим более подробно подстановку параметров.

Условия

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

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

Команда test или [

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

Примечание

Эти команды в некоторых ранних оболочках UNIX вызывают внешнюю программу, но в более современных версиях их стараются делать встроенными. Мы вернемся к этому, когда будем рассматривать команды в следующем разделе.

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

Мы представим команду test на примере одного простейшего условия: проверки наличия файла. Для нее понадобится следующая команда: test -f <имя_файла>, поэтому в сценарии можно написать

if test -f fred.c

then

 ...

fi

To же самое можно записать следующим образом:

if [ -f fred.c ]

then

 ...

fi

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

Примечание

Имейте в виду, что вы должны вставлять пробелы между квадратной скобкой [ и проверяемым условием. Это легко усвоить, если запомнить, что вставить символ [ — это все равно, что написать test, а после имени команды вы всегда должны вставлять пробел.

Если вы предпочитаете помещать слово then в той же строке, что и if, нужно добавить точку с запятой для отделения команды test от then:

if [ -f fred.c ]; then

 ...

fi

Варианты условий, которые вы можете применять в команде test, делятся на три типа: строковые сравнения, числовые сравнения и проверка файловых флагов (file conditionals). Эти типы условий описаны в табл. 2.4.

Таблица 2.4

Варианты условий Результат
Сравнения строк
Строка1 = Строка2 True (истина), если строки одинаковы
Строка1 != Строка2 True (истина), если строки разные
-n Строка True (истина), если Строка не null
-z Строка True (истина), если Строка null (пустая строка)
Сравнения чисел
Выражение1 -eq Выражение2 True (истина), если выражения равны
Выражение1 -ne Выражение2 True (истина), если выражения не равны
Выражение1 -gt Выражение2 True (истина), если Выражение1 больше, чем Выражение2
Выражение1 -ge Выражение2 True (истина), если Выражение1 не меньше Выражение2
Выражение1 -lt Выражение2 True (истина), если Выражение1 меньше, чем Выражение2
Выражение1 -lе Выражение2 True (истина), если Выражение1 не больше Выражение2
! Выражение True (истина), если Выражение ложно, и наоборот
Файловый флаг
-d файл True (истина), если файл — каталог
файл True (истина), если файл существует. Исторически, опция -e не была переносима на другие платформы, поэтому обычно применяется -f
-f файл True (истина), если файл — обычный файл
-g файл True (истина), если для файла установлен бит set-group-id
-r файл True (истина), если файл доступен для чтения
-s файл True (истина), если файл ненулевого размера
-u файл True (истина), если для файла установлен бит set-user-id
-v файл True (истина), если файл доступен для записи
файл True (истина), если файл — исполняемый файл

Примечание

Вас могли заинтересовать непонятные биты set-group-id и set-user-id (также называемые set-gid и set-uid). Бит set-uid предоставляет программе права владельца, а не просто ее пользователя, бит set-gid предоставляет программе права группы. Эти биты устанавливаются командой chmod с помощью опций s и g. На файлы, содержащие сценарии, флаги set-gid и set-uid не влияют, они оказывают влияние только на исполняемые двоичные файлы.

Мы немного сами себя обогнали, но далее следует пример тестирования состояния файла /bin/bash, так что вы сможете увидеть, как это выглядит на практике.

#!/bin/sh

if [ -f /bin/bash ]

then

 echo "file /bin/bash exists"

fi

if [ -d /bin/bash ]

then

 echo "/bin/bash is a directory"

else

 echo "/bin/bash is NOT a directory"

fi

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

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

Управляющие структуры

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

Примечание

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

if

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

if условие

then

 операторы

else

 операторы

fi

Наиболее часто оператор if применяется, когда задается вопрос, и решение принимается в зависимости от ответа:

#!/bin/sh

echo "Is it morning? Please answer yes or no "

read timeofday

if [ $timeofday = "yes" ]; then

 echo "Good morning"

else

 echo "Good afternoon"

fi

exit 0

В результате будет получен следующий вывод на экран:

Is it morning? Please answer yes or no

yes

Good morning

$

В этом сценарии для проверки содержимого переменной timeofday применяется команда [. Результат оценивается оператором командной оболочки if, который затем разрешает выполнять разные строки программного кода.

Примечание

Обратите внимание на дополнительные пробелы, используемые для формирования отступа внутри оператора if. Это делается только для удобства читателя; командная оболочка игнорирует дополнительные пробелы.

elif

К сожалению, с этим простым сценарием связано несколько проблем. Во-первых, он принимает в значении no (нет) любой ответ за исключением yes (да). Можно помешать этому, воспользовавшись конструкцией elif, которая позволяет добавить второе условие, проверяемое при выполнении части else оператора if (упражнение 2.3). 

Упражнение 2.3. Выполнение проверок с помощью elif

Вы можете откорректировать предыдущий сценарий так, чтобы он выводил сообщение об ошибке, если пользователь вводит что-либо отличное от yes или no. Для этого замените ветку else веткой elif и добавьте еще одно условие:

#!/bin/sh

echo "Is it morning? Please answer yes or no "

read timeofday

if [ $timeofday = "yes" ]

then

 echo "Good morning"

elif [ $timeofday = "no" ]; then

 echo "Good afternoon"

else

 echo "Sorry, $timeofday not recognized. Enter yes or no "

 exit 1

fi

exit 0

Как это работает

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

Проблема, связанная с переменными

Данный сценарий исправляет наиболее очевидный дефект, а более тонкая проблема остается незамеченной. Запустите новый вариант сценария, но вместо ответа на вопрос просто нажмите клавишу <Enter> (или на некоторых клавиатурах клавишу <Return>). Вы получите сообщение об ошибке:

[: =: unary operator expected

Что же не так? Проблема в первой ветви оператора if. Когда проверялась переменная timeofday, она состояла из пустой строки. Следовательно, ветвь оператора if выглядела следующим образом:

if [ = "yes" ]

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

if [ "$timeofday" = "yes" ]

Теперь проверка с пустой переменной будет корректной:

if [ "" = "yes" ]

Новый сценарий будет таким:

#!/bin/sh

echo "Is it morning? Please answer yes or no "

read timeofday

if [ "$timeofday" = "yes" ]

then

 echo "Good morning"

elif [ "$timeofday" = "no" ]; then

 echo "Good afternoon"

else

 echo "Sorry, $timeofday not recognized. Enter yes or no "

 exit 1

fi

exit 0

Этот вариант безопасен, даже если пользователь в ответ на вопрос просто нажмет клавишу <Enter>.

Примечание

Если вы хотите, чтобы команда echo удалила новую строку в конце, наиболее легко переносимый вариант — применить команду printf (см. разд. "printf" далее в этой главе) вместо команды echo. В некоторых командных оболочках применяется команда echo -е, но она поддерживается не всеми системами. В оболочке bash для запрета перехода на новую строку допускается команда echo -n, поэтому, если вы уверены, что вашему сценарию придется трудиться только в оболочке bash, предлагаем вам использовать следующий синтаксис:

echo -n "Is it morning? Please answer yes or no: "

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

for

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

Синтаксис этого оператора прост:

for переменная in значения

do

 операторы

done

Выполните упражнения 2.4 и 2.5.

Упражнение 2.4. Применение цикла for к фиксированным строкам

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

#!/bin/sh

for foo in bar fud 43

do

 echo $foo

done

exit 0

В результате будет получен следующий вывод:

bar

fud

43

Примечание

Что произойдет, если вы измените первую строку с for foo in bar fud 43 на for foo in "bar fud 43"? Напоминаем, что вставка кавычек заставляет командную оболочку считать все, что находится между ними, единой строкой. Это один из способов сохранения пробелов в переменной.

Как это работает

В данном примере создается переменная foo и ей в каждом проходе цикла for присваиваются разные значения. Поскольку оболочка считает по умолчанию все переменные строковыми, применять строку 43 так же допустимо, как и строку fud.

Упражнение 2.5. Применение цикла for с метасимволами

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

Вы уже видели этот прием в первом примере first. В сценарии применялись средства подстановки командной оболочки — символ * для подстановки имен всех файлов из текущего каталога. Каждое из этих имен по очереди используется в качестве значения переменной $file внутри цикла for.

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

#!/bin/sh

for file in $(ls f*.sh); do

 lpr $file

done

exit 0

Как это работает

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

Командная оболочка раскрывает f*.sh, подставляя имена всех файлов, соответствующих данному шаблону.

Примечание

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

while

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

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

while  условие

do

 операторы

done

Далее приведен пример довольно слабой программы проверки паролей.

#!/bin/sh

echo "Enter password"

read trythis

while [ "$trythis" != "secret" ]; do

 echo "Sorry, try again"

 read trythis

done

exit 0

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

Enter password

password

Sorry, try again

secret

$

Ясно, что это небезопасный способ выяснения пароля, но он вполне подходит для демонстрации применения цикла while. Операторы, находящиеся между операторами do и done, выполняются бесконечное число раз до тех пор, пока условие остается истинным (true). В данном случае вы проверяете, равно ли значение переменной trythis строке secret. Цикл будет выполняться, пока $trythis не равно secret. Затем выполнение сценария продолжится с оператора, следующего сразу за оператором done.

until

У цикла until следующая синтаксическая запись:

until  условие

do

 операторы

done

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

Примечание

Как правило, если нужно выполнить цикл хотя бы один раз, применяют цикл while; если такой необходимости нет, используют цикл until.

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

#!/bin/bash

until who | grep "$1" > /dev/null

do

 sleep 60

done

# Теперь звонит колокольчик и извещает о новом пользователе

echo -е '\а'

echo "**** $1 has just logged in ****"

exit 0

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

case

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

case переменная  in

 образец [ | образец] ...) операторы;;

 образец [ | образец] ...) операторы;;

esac

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

Примечание

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

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

Примечание

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

Упражнение 2.6. Вариант 1: пользовательский ввод

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

#!/bin/sh

echo "Is it morning? Please answer yes or no "

read timeofday

case "$timeofday" in

 yes) echo "Good Morning";;

 no ) echo "Good Afternoon";;

 y  ) echo "Good Morning";;

 n  ) echo "Good Afternoon";;

 *  ) echo "Sorry, answer not recognized";;

esac

exit 0

Как это работает

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

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

Упражнение 2.7. Вариант 3: объединение образцов

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

#!/bin/sh

echo "Is it morning? Please answer yes or no "

read timeofday

case "$timeofday" in

 yes | y | Yes | YES ) echo "Good Morning";;

 n* | N*)              echo "Good Afternoon";;

 * )                   echo "Sorry, answer not recognized";;

esac

exit 0

Как это работает

Данный сценарий в операторе case использует несколько строк-образцов в каждой ветви, таким образом, case проверяет несколько разных строк для каждого возможного оператора. Этот прием делает сценарий короче и, как показывает практика, облегчает его чтение. Приведенный программный код также показывает, как можно использовать метасимвол *, несмотря на то, что он может соответствовать непредусмотренным образцам. Например, если пользователь введет строку never, она будет соответствовать образцу n*, и на экран будет выведено приветствие Good Afternoon (Добрый день), хотя такое поведение в сценарии не предусматривалось. Учтите также, что заключенный в кавычки знак подстановки * не действует.

Упражнение 2.8. Вариант 3: выполнение нескольких операторов

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

#!/bin/sh

echo "Is it -morning? Please answer yes or no"

read timeofday

case "$timeofday" in

 yes | y | Yes | YES )

  echo "Good Morning"

  echo "Up bright and early this morning"

  ;;

 [nN]*)

  echo "Good Afternoon"

  ;;

 *)

  echo "Sorry, answer not recognized"

  echo "Please answer yes or no"

  exit 1

  ;;

esac

exit 0

Как это работает

Для демонстрации другого способа определения соответствия образцу в этом программном коде изменен вариант определения соответствия для ветви no. Также видно, как в каждой ветви оператора case может выполняться несколько операторов. Следует быть внимательным и располагать в операторе самые точные образцы строк первыми, а самые общие варианты образцов последними. Это очень важно, потому что оператор case выполняется, как только найдено первое, а не наилучшее соответствие. Если вы поставите ветвь *) первой, совпадение с этим образцом будет определяться всегда, независимо от варианта введенной строки.

Примечание

Учтите, что сдвоенная точка с запятой ;; перед служебным словом esac необязательна. В отличие от программирования на языке С, в котором пропуск маркера завершения считается плохим стилем программирования, пропуск ;; не создает проблем, если последняя ветвь оператора case — это вариант, принятый по умолчанию, поскольку другие образцы уже не будут анализироваться.

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

[yY] | [Yy][Ее][Ss])

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

Списки

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

if [ -f this_file ]; then

 if [ -f that_file ]; then

  if [ -f the_other_file ]; then

   echo "All files present, and correct"

  fi

 fi

fi

Или вы хотите, чтобы хотя бы одно условие из последовательности условий было истинным.

if [ -f this_file ]; then

 foo="True"

elif [ -f that_file ]; then

 foo="True"

elif [ -f the_other_file ];

 then foo="True"

else

 foo="False"

fi

if ["$foo" = "True" ]; then

 echo "One of the files exists"

fi

Несмотря на то, что это можно реализовать с помощью нескольких операторов if, как видите, результаты получаются очень громоздкими. В командной оболочке есть пара специальных конструкций для работы со списками команд: И-список (AND list) и ИЛИ-список (OR list). Обе они часто применяются вместе, но мы рассмотрим синтаксическую запись каждой из них отдельно.

И-cписок

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

оператор1 && оператор2 && оператор3  && ...

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

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

Выполните упражнение 2.9.

Упражнение 2.9. И-списки

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

#!/bin/sh

touch file_one

rm -f file_two

if [ -f file_one ] && echo "hello" [ -f file_two ] && echo " there"

then

 echo "in if"

else

 echo "in else"

fi

exit 0

Попробуйте выполнить сценарий, и вы получите следующий вывод:

hello

in else

Как это работает

Команды touch и rm гарантируют, что файлы в текущем каталоге находятся в известном состоянии. Далее И-список выполняет команду [ -f file one ], которая возвращает значение true, потому что вы только что убедились в наличии файла. Поскольку предыдущий оператор завершился успешно, теперь выполняется команда echo. Она тоже завершается успешно (echo всегда возвращает true). Затем выполняется третья проверка [ -f file_two ]. Она возвращает значение false, т.к. файл не существует. Поскольку последняя команда вернула false, заключительная команда echo не выполняется. В результате И-список возвращает значение false, поэтому в операторе if выполняется вариант else.

ИЛИ-список

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

оператор1 || оператор2 || оператор3 || ...

Операторы выполняются слева направо. Если очередной оператор возвращает значение false, выполняется следующий за ним оператор. Это продолжается до тех пор, пока очередной оператор не вернет значение true, после этого никакие операторы уже не выполняются.

ИЛИ-список очень похож на И-список, за исключением того, что правило для выполнения следующего оператора — выполнение предыдущего оператора со значением false.

Рассмотрим упражнение 2.10.

Упражнение 2.10. ИЛИ-списки

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

#!/bin/sh

rm -f file_one

if [ -f file_one ] || echo "hello" || echo " there" then

 echo "in if"

else

 echo "in else"

fi

exit 0

В результате выполнения данного сценария будет получен следующий вывод:

hello

in if

Как это работает

В первых двух строках просто задаются файлы для остальной части сценария. Первая команда списка [ -f file one ] возвращает значение false, потому что файла в каталоге нет. Далее выполняется команда echo. Вот это да — она возвращает значение true, и больше в ИЛИ-списке не выполняются никакие команды. Оператор if получает из списка значение true, поскольку одна из команд ИЛИ-списка (команда echo) вернула это значение.

Результат, возвращаемый обоими этими списками, — это результат последней выполненной команды списка.

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

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

[ -f file_one ] && команда в случае true || команда в случае false

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

Операторные блоки

Если вы хотите применить несколько операторов в том месте программного кода, где разрешен только один, например в ИЛИ-списке или И-списке, то можете сделать это, заключив операторы в фигурные скобки {} и создав тем самым операторный блок. Например, в приложении, представленном далее в этой главе, вы увидите следующий фрагмент программного кода:

get_confirm && {

 grep -v "$cdcatnum" $tracks_file > $temp_file

 cat $temp_file > $tracks_file

 echo

 add record_tracks

}

Функции

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

Примечание

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

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

Имя_функции() {

операторы

}

Выполните упражнения 2.11 и 2.12.

Упражнение 2.11. Простая функция

Давайте начнем с действительно простой функции.

#!/bin/sh

foo() {

 echo "Function foo is executing"

}

echo "script starting"

foo

echo "script ended"

exit 0

Выполняющийся сценарий, выведет на экран следующий текст:

script starting

Function foo is executingscript ended

Как это работает

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

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

Когда функция вызывается, позиционные параметры сценария $*, $@, $#, $1, $2 и т.д. заменяются параметрами функции. Именно так вы считываете параметры, передаваемые функции. Когда функция завершится, они восстановят свои прежние значения.

Примечание

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

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

foo() { echo JAY;}

...

result="$(foo)"

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

#!/bin/sh

sample_text="global variable"

foo() {

 local sample_text="local variable"

 echo "Function foo is executing"

 echo $sample_text

}

echo "script starting"

echo $sample_text

foo

echo "script ended"

echo $sample_text

exit 0

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

Упражнение 2.12. Возврат значения

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

1. После заголовка командной оболочки определите функцию yes_or_no.

#!/bin/sh

yes_or_no() {

 echo "Is your name $* ? "

 while true

 do

  echo -n "Enter yes or no: "

  read x

  case "$x" in

   y | yes ) return 0;;

   n | no )  return 1;;

   * )       echo "Answer yes or no"

  esac

 done

}

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

echo "Original parameters are $*"

if yes_or_no "$1"

then

 echo "Hi $1, nice name"

else

 echo "Never mind"

fi

exit 0

Типичный вывод этого сценария может выглядеть следующим образом:

$ ./my_name Rick Neil

Original parameters are Rick Neil

Is your name Rick ?

Enter yes or no: yes

Hi Rick, nice name

$

Как это работает

Когда сценарий начинает выполняться, функция определена, но еще не выполняется. В операторе if сценарий вызывает функцию yes_or_no, передавая ей оставшуюся часть строки как параметры после замены $1 первым параметром исходного сценария строкой Rick. Функция использует эти параметры, в данный момент хранящиеся в позиционных параметрах $1, $2 и т.д., и возвращает значение в вызывающую программу. В зависимости от возвращенного функцией значения конструкция if выполняет один из операторов.

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

Команды

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

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

break

Используйте команду break для выхода из циклов for, while и until до того, как будет удовлетворено управляющее условие. В команде break можно задать дополнительный числовой параметр, указывающий на число циклов, из которых предполагается выход. Однако это может сильно усложнить чтение сценариев, поэтому мы не советуем вам использовать его. По умолчанию break обеспечивает выход из одного цикла.

#!/bin/sh

rm -rf fred*

echo > fred1

echo > fred2

mkdir fred3

echo > fred4

for file in fred*

do

 if [ -d "$file" ]; then

  break;

 fi

done

echo first directory starting fred was $file

m -rf fred*

exit 0

Команда :

Команда "двоеточие" — фиктивная команда. Она иногда полезна для упрощения логики в условиях, будучи псевдонимом команды true. Поскольку команда : встроенная, она выполняется быстрее, чем true, хотя ее вывод гораздо менее читабелен.

Вы можете найти эту команду в условии для циклов while. Конструкция while : выполняет бесконечный цикл вместо более общего while true.

Конструкция : также полезна для условного задания переменных. Например,

: ${var:=value}

Без : командная оболочка попытается интерпретировать $var как команду.

Примечание

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

#!/bin/sh

rm -f fred

if [ -f fred ]; then

 :

else

 echo file fred did not exist

fi

exit 0

continue

Как и одноименный оператор языка С, эта команда заставляет охватывающий ее цикл for, while или until начать новый проход или следующую итерацию. При этом переменная цикла принимает следующее значение в списке.

#!/bin/sh

rm -rf fred*

echo > fred1

echo > fred2

mkdir fred3

echo > fred4

for file in fred*

do

 if [ -d "$file" ]; then

  echo "skipping directory $file"

  continue

 fi

 echo file is $file

done

rm -rf fred*

exit 0

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

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

for x in 1 2 3

do

 echo before $x

 continue 1

 echo after $x

done

У приведенного фрагмента будет следующий вывод:

before 1

before 2

before 3

Команда .

Команда "точка" (.) выполняет команду в текущей оболочке:

. ./shell_script

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

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

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

Выполните упражнение 2.13.

Упражнение 2.13. Команда точка

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

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

#!/bin/sh

version=classic

PATH=/usr/local/old_bin:/usr/bin:/bin:

.

PS1="classic> "

2. Для новых команд применяется latest_set.

#!/bin/sh

version=latest

PATH=/usr/local/new_bin:/usr/bin:/bin:

.

PS1=" latest version> "

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

$ . ./classic_set

classic> echo $version

classic

classic> . /latest_set

latest version> echo $version

latest

latest version>

Как это работает

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

echo

Несмотря на призыв группы Х/Open применять в современных командных оболочках команду printf, мы будем продолжать следовать общепринятой практике использования команды echo для вывода строки с последующим переходом на новую строку.

При этом возникает общая проблема: удаление символа перехода на новую строку. К сожалению, в разных версиях ОС UNIX реализованы разные решения. В ОС Linux общепринятый метод

echo -n "string to output"

Но вы часто будете сталкиваться и с вариантом

echo -е "string to output\c"

Второй вариант echo -е рассчитан на то, что задействована интерпретация символов escape-последовательности, начинающихся с обратного слэша, таких как \c для подавления новой строки, \t для вывода табуляции, \n для вывода символов возврата каретки. В более старых версиях bash этот режим установлен по умолчанию, а в более современных версиях интерпретация символов escape-последовательностей с обратным слэшем отключена. Подробные сведения о поведении вашего дистрибутива ищите на страницах интерактивного справочного руководства.

Примечание

Если вам нужен легко переносимый способ удаления завершающей новой строки, для избавления от нее можно воспользоваться внешней командой tr, но она будет выполняться немного медленнее. Если вашим системам UNIX нужна переносимость и нужно избавиться от завершающей новой строки, как правило, лучше придерживаться команды printf. Если ваши сценарии предназначены для работы только в ОС Linux и bash, вполне подойдет echo -n, хотя, возможно, придется начинать файл со строки #!/bin/bash для того, чтобы в явной форме показать, что вы рассчитываете на поведение в стиле bash.

eval

Команда eval позволяет вычислять аргументы. Она встроена в командную оболочку и обычно не представлена как отдельная команда. Лучше всего ее действие демонстрирует короткий пример, позаимствованный непосредственно из стандарта X/Open.

foo=10

x=foo

у='$'$х

echo $у

Будет выведено $foo. Однако код

foo=10

x=foo

eval у='$'$х

echo $у

выведет на экран 10. Таким образом, eval немного похожа на дополнительный знак $: она возвращает значение значения переменной.

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

exec

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

Например, строка

exec wall "Thanks for all the fish"

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

Второй вариант применения exec — модификация текущих дескрипторов файлов.

exec 3< afile

Эта команда открывает файловый дескриптор 3 для чтения из файла afile. Этот вариант редко используется.

exit n

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

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

Таблица 2.5

Код завершения Описание
126 Файл не является исполняемым
127 Команда не найдена
128 и выше Появившийся сигнал

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

Далее приведен простой пример, возвращающий код успешного завершения, если в текущем каталоге существует файл с именем .profile.

#!/bin/sh

if [ -f .profile ]; then

 exit 0

fi

exit 1

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

[ -f .profile ] && exit 0 || exit 1

export

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

Упражнение 2.14. Экспорт переменных

1. Первым представим сценарий export2.

#!/bin/sh

echo "$foo"

echo "$bar"

2. Теперь сценарий export1. В конце сценария запускается export2.

#!/bin/sh

foo="The first meta-syntactic variable"

export bar="The second meta-syntactic variable"

export2

Если вы запустите их, то получите следующий результат.

$ ./export1

The second meta-syntactic variable

$

Как это работает

Сценарий export2 просто выводит значения двух переменных. В сценарии export1 задаются значения обеих переменных, но только переменная bar помечается как экспортируемая, поэтому, когда впоследствии запускается сценарий export2, значение переменной foo потеряно, а значение переменной bar экспортировано во второй сценарий. На экране появляется пустая строка, поскольку $foo ничего не содержит и вывод переменной со значением null приводит к отображению новой строки.

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

Примечание

Команды set -а или set -allexport экспортируют все переменные соответственно.

expr

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

х=`expr $x + 1`

Символы `` (обратная кавычка или обратный апостроф) заставляют переменную х принять результат выполнения команды expr $х + 1. Ее можно также записать с помощью синтаксической конструкции $( ) вместо обратной кавычки, например, следующим образом:

х=$(expr $х + 1)

Команда expr обладает большими возможностями, с ее помощью можно вычислять различные выражения. Основные виды вычислений перечислены в табл. 2.6.

Таблица 2.6

Вычисление выражения Описания
Выражение1 | Выражение2 Выражение1, если Выражение1 не равно нулю, в противном случае Выражение2
Выражение1 & Выражение2 Нуль, если оба выражения равны нулю, в противном случае Выражение1
Выражение1 = Выражение2 Равенство
Выражение1 > Выражение2 Больше чем
Выражение1 >= Выражение2 Больше или равно
Выражение1 < Выражение2 Меньше чем
Выражение1 <= Выражение2 Меньше или равно
Выражение1 != Выражение2 Неравенство
Выражение1 + Выражение2 Сложение
Выражение1Выражение2 Вычитание
Выражение1 * Выражение2 Умножение
Выражение1 / Выражение2 Деление нацело
Выражение1 % Выражение2 Остаток от деления нацело

В современных сценариях вместо команды expr обычно применяется более эффективная синтаксическая конструкция $((...)), которая будет описана далее в этой главе.

printf

Команда printf есть только в современных командных оболочках. Группа X/Open полагает, что ее следует применять вместо команды echo для генерации форматированного вывода, несмотря на то, что, кажется, лишь немногие следуют этому совету.

У команды следующая синтаксическая запись.

printf "строка формата" параметр1 параметр2 ...

Строка формата очень похожа с некоторыми ограничениями на применяемую в языках программирования С и С++. Главным образом не поддерживаются числа с плавающей точкой, поскольку все арифметические операции в командной оболочке выполняются над целыми числами. Строка формата состоит из произвольной комбинации литеральных символов, escape-последовательностей и спецификаторов преобразования. Все символы строки формата, отличающиеся от \ и %, отображаются на экране при выводе.

В табл. 2.7 приведены поддерживаемые командой escape-последовательности.

Таблица 2.7

Escape-последовательность Описание
\" Двойная кавычка
\\ Символ обратный слэш
\a Звуковой сигнал тревоги (звонок колокольчика или прерывистый звуковой сигнал)
\b Символ Backspace (стирание слева)
\c Отбрасывание последующего вывода
\f Символ Form feed (подача бумаги)
\n Символ перехода на новую строку
\r Возврат каретки
\t Символ табуляции
\v Символ вертикальной табуляции
\ooo Один символ с восьмеричным значением ooo
\xHH Один символ с шестнадцатеричным значением HH

Спецификаторы преобразований довольно сложны, поэтому мы приведем наиболее распространенные варианты их применения. Более подробную информацию можно найти в интерактивном справочном руководстве командной оболочки bash или на страницах раздела 1 интерактивного руководства к команде printf (man 1 printf). (Если вы не найдете нужных сведений в разделе 1, попробуйте поискать в разделе 3.) Спецификатор преобразования состоит из символа %, за которым следует символ преобразования. Основные варианты преобразований перечислены в табл. 2.8.

Таблица 2.8

Символ преобразования  Описание
D Вывод десятичного числа
С Вывод символа
S Вывод строки
% Вывод знака %

Строка формата используется для интерпретации остальных параметров команды и вывода результата, как показано в следующем примере:

$ printf "%s\n" hello

hello

$ printf "%s %d\t%s" "Hi There" 15 people

Hi There 15 people

Обратите внимание на то, что для защиты строки Hi There и превращения ее в единый параметр, строку нужно заключить в кавычки ("").

return

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

set

Команда set задает переменные-параметры командной оболочки. Она может быть полезна при использовании полей в командах, выводящих значения, разделенные пробелами.

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

#!/bin/sh

echo the date is $(date)

set $(date)

echo The month is $2

exit 0

Программа задает список параметров для вывода команды date и затем использует позиционный параметр $2 для получения названия месяца.

Мы использовали команду date только как простой пример, демонстрирующий, как извлекать позиционные параметры. Поскольку команда date зависит от языковых параметров или локализации, в действительности мы бы извлекли название месяца командой date +%B. У команды date много других вариантов форматирования, более подробную информацию см. на страницах интерактивного справочного руководства к команде.

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

shift

Команда shift сдвигает все переменные-параметры на одну позицию назад, так что параметр $2 становится параметром $1, параметр $3$2 и т.д. Предыдущее значение параметра $1 отбрасывается, а значение параметра $0 остается неизменным. Если в вызове команды shift задан числовой параметр, параметры сдвигаются на указанное количество позиций. Остальные переменные $*, $@ и $# также изменяются в связи с новой расстановкой переменных-параметров.

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

Например, вы можете просмотреть все позиционные параметры:

#!/bin/sh

while [ "$1" != "" ]; do

 echo "$1"

 shift

done

exit 0

trap

Команда trap применяется для задания действий, предпринимаемых при получении сигналов, которые подробно будут обсуждаться далее в этой книге. Обычное действие — удалить сценарий, когда он прерван. Исторически командные оболочки всегда использовали числа для обозначения сигналов, но в современных сценариях следует применять имена, которые берутся из файла signal.h директивы #include с опущенным префиксом SIG. Для того чтобы посмотреть номера сигналов и соответствующие им имена, можно ввести в командной строке команду trap -l.

Примечание

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

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

trap команда сигнал

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

Для возврата к стандартной реакции на сигнал, просто задайте команду как -. Для игнорирования сигнала задайте в команде пустую строку ''. Команда trap без параметров выводит текущий список перехватов и действий.

В табл. 2.9 перечислены самые важные, включенные в. стандарт Х/Open сигналы, которые можно отследить (со стандартными номерами в скобках). Дополнительную информацию можно найти на страницах раздела 7 интерактивного справочного руководства, посвященного сигналам (man 7 signal).

Таблица 2.9

Сигнал Описание
HUP (1) Неожиданный останов; обычно посылается, когда отключается терминал или пользователь выходит из системы
INT (2) Прерывание; обычно посылается нажатием комбинации клавиш <Ctrl>+<C>
QUIT (3) Завершение выполнения; обычно посылается нажатием комбинации клавиш <Ctrl>+<\>
ABRT (6) Аварийное завершение; обычно посылается при возникновении серьезной ошибки выполнения
ALRM (14) Аварийный сигнал; обычно посылается для обработки превышений лимита времени
TERM (15) Завершение; обычно посылается системой, когда она завершает работу

А теперь выполните упражнение 2.15.

Упражнение 2.15. Сигналы прерываний

В следующем сценарии показана простая обработка сигнала.

#!/bin/sh

trap 'rm -f /tmp/my_tmp_file_$$' INT

echo creating file /tmp/my_tmp_file_$$

date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt..."

while [ -f /tmp/my_tmp_file_$$ ] ; do

 echo File exists

 sleep 1

done

echo The file no longer exists trap INT

echo creating file /tmp/my_tmp_file_$$

date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt..."

while [ -f /tmp/my_tmp_file_$$ ]; do

 echo File exists

 sleep 1

done

echo we never get here

exit 0

Если вы выполните этот сценарий, нажимая и удерживая нажатой клавишу <Ctrl> и затем нажимая клавишу <C> (или любую другую прерывающую комбинацию клавиш) в каждом из циклов, то получите следующий вывод:

creating file /tmp/my_tmp_file_141

press interrupt (CTRL-C) to interrupt ...

File exists

File exists

File exists

File exists

The file no longer exists

creating file /tmp/my tmp_file_141

press interrupt (CTRL-C) to interrupt ...

File exists

File exists

File exists

File exists

Как это работает

Сценарий использует команду trap для организации выполнения команды rm -f /tmp/my_tmp_file_$$ при возникновении сигнала INT (прерывание). Затем сценарий выполняет цикл while до тех пор, пока существует файл. Когда пользователь нажимает комбинацию клавиш <Ctrl>+<C>, выполняется команда rm -f /tmp/my_tmp_file_$$, а затем возобновляется выполнение цикла while. Поскольку теперь файл удален, первый цикл while завершается стандартным образом.

Далее сценарий снова применяет команду trap, на этот раз для того, чтобы сообщить, что при возникновении сигнала INT никакая команда не выполняется. Затем сценарий создает заново файл и выполняет второй цикл while. Когда пользователь снова нажимает комбинацию клавиш <Ctrl>+<C>, не задана команда для выполнения, поэтому реализуется стандартное поведение: немедленное прекращение выполнения сценария. Поскольку сценарий завершается немедленно, заключительные команды echo и exit никогда не выполняются.

unset

Команда unset удаляет переменные или функции из окружения. Она не может проделать это с переменными, предназначенными только для чтения и определенными командной оболочкой, такими как IFS. Команда применяется редко.

В следующем сценарии сначала выводится строка Hello world, а во второй раз новая строка.

#!/bin/sh

foo="Hello World"

echo $foo

unset foo

echo $foo

Примечание

Написание foo= подобно, но не идентично применению команды unset в только что приведенной программе. Оператор foo= задает для переменной foo значение null, но при этом переменная foo все еще существует. Команда unset foo удаляет из окружения переменную foo.

Еще две полезные команды и регулярные выражения

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

Команда find

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

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

# find / -name test -print

/usr/bin/test

#

В зависимости от варианта установки системы на вашей машине вы можете найти и другие файлы, также названные test. Как вы, вероятно, догадываетесь, команда звучит так: "искать, начиная с каталога /, файл с именем test и затем вывести на экран имя файла". Легко, не правда ли? Безусловно.

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

В этом случае на помощь приходит первая опция. Если вы укажете опцию -mount, то сможете сообщить команде find о том, что смонтированные каталоги проверять не нужно.

# find / -mount -name test -print

/usr/bin/test

#

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

Полная синтаксическая запись команды find выглядит следующим образом:

find [путь] [опции] [критерии] [действия]

Часть записи [путь] понятна и проста: вы можете указать абсолютный путь поиска, например, /bin, или относительный, например .. При необходимости можно задать несколько путей — например, find /var /home.

В табл. 2.10 перечислены основные опции команды.

Таблица 2.10

Опция Описание
-depth Поиск в подкаталогах перед поиском в самом каталоге
-follow Следовать по символическим ссылкам
-maxdepths N При поиске проверять не более N вложенных уровней каталога
-mount (или -xdev) Не искать в каталогах других файловых систем

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

Таблица 2.11

Критерий Описание
-atime N К файлу обращались последний раз N дней назад
-mtime N Файл последний раз изменялся N дней назад
-name шаблон Имя файла без указания пути соответствует заданному шаблону. Для гарантии того, что шаблон будет передан в команду find и не будет немедленно обработан командной оболочкой, его следует всегда заключать в кавычки
-newer другой файл Текущий файл, измененный позже, чем другой файл
-type С Файл типа C, где C может принимать определенные значения; наиболее широко используемые "d" для каталогов и "f" для обычных файлов. Остальные обозначения типов можно посмотреть на страницах интерактивного справочного руководства
-user имя пользователя Файл принадлежит пользователю с заданным именем

Вы также можете объединять критерии с помощью операторов. Как показано в табл. 2.12, у большинства из них две формы записи: короткая и более длинная форма.

Таблица 2.12

Оператор, короткая форма Оператор, длинная форма Описание
! -not Инвертирование критерия
-and Оба критерия должны быть истинны
-or Один из критериев должен быть истинным

Изменить порядок проверки критериев и выполнения операторов можно с помощью скобок. Поскольку в командной оболочке у них есть особое назначение, скобки также следует выделять с помощью обратного слэша. Кроме того, если вы применяете шаблон для имени файла, то следует использовать кавычки, чтобы оболочка не выполняла подстановку имени, а прямо передала шаблон команде find. Например, если вы хотите задать критерий "измененный позже, чем файл X, или с именем, начинающимся со знака подчеркивания", его можно записать следующим образом:

\(-newer X -о -name "_*" \)

Мы приведем пример сразу после описания "Как это работает". А сейчас выполните упражнение 2.16.

Упражнение 2.16 Применение команды find с критериями

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

$ find . -newer while2 -print

.

./elif3

./words.txt

./words2.txt

./_trap

$

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

$ find . -newer while2 -type f -print

./elif3

./words.txt

./words2.txt

./_trap

$

Как это работает

Как это работает? Вы определили, что команда find должна искать в текущем каталоге (.) файлы, измененные позже, чем файл while2 (-newer while2), и, если этот критерий пройден, проверять с помощью следующего критерия (-type f), обычные ли это файлы. В заключение вы применили действие, с которым уже сталкивались, -print, просто для того чтобы подтвердить, что файлы были найдены.

Теперь найдем файлы с именами, начинающимися с символа подчеркивания или измененные позже, чем файл while2, но в любом случае обычные файлы. Этот пример покажет, как объединять критерии с помощью скобок.

$ find . \( -name "_*" -or -newer while2 \) -type f -print

./elif3

./words.txt

./words2.txt

./_break

./_if

./set

./_shift

./_trap

./_unset

./ until

$

Это не слишком трудный пример, не так ли? Вы должны экранировать скобки, чтобы они не обрабатывались командной оболочкой, и заключить в кавычки символ *, чтобы он также был передан непосредственно в команду find.

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

Таблица 2.13

Действие Описание
-exec команда Выполняеткоманду. Наиболее широко используемое действие. После табл. 2.13 приведено объяснение способа передачи параметров в команду. Это действие следует завершать символьной парой \;
-ok команда Подобно действию exec, за исключением того, что перед обработкой файловкомандой выводится подсказка для получения подтверждения пользователя на обработку каждого файла. Это действие следует завершать символьной парой \;
-print Вывод на экран имени файла
-ls Применение команды ls -dils к текущему файлу

Команда в аргументах -exec и -ok принимает последующие параметры в строке как собственные, пока не встретится последовательность \; В действительности команда, в аргументах -exec и -ok выполняет встроенную команду, поэтому встроенная команда должна завершиться экранированной точкой с запятой, для того чтобы команда find могла определить, когда ей следует продолжить поиск в командной строке аргументов, предназначенных для нее самой. Магическая строка {} — параметр специального типа для команд -exec и -ok, который заменяется полным путем к текущему файлу.

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

$ find . -newer while2 -type f -exec ls -l  {} \;

-rwxr-xr-x 1 rick rick  275 Feb 8 17:07 ./elif3

-rwxr-xr-x 1 rick rick  336 Feb 8 16:52 ./words.txt

-rwxr-xr-x 1 rick rick 1274 Feb 8 16:52 ./words2.txt

-rwxr-xr-x 1 rick rick  504 Feb 8 18:43 ./_trap

$

Как видите, команда find чрезвычайно полезна; она только требует небольшой практики для умелого ее применения. И такая практика, как и эксперименты с командой find, обязательно принесет дивиденды.

Команда grep

Вторая очень полезная команда, заслуживающая рассмотрения, — это команда grep. Необычное имя, означающее общий синтаксический анализатор регулярных выражений (general regular expression parser). Вы применяете команду find для поиска файлов в вашей системе, а команду grep для поиска строк в ваших файлах. Действительно, очень часто при использовании команды find команда grep передается после аргумента -exec.

Команда grep принимает опции, шаблон соответствия и файлы для поиска:

grep [опции] шаблон [файлы]

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

Давайте начнем с изучения основных опций команды grep. И на этот раз в табл. 2.14 приведены только самые важные из них; полный список см. на страницах интерактивного справочного руководства.

Таблица 2.14

Опция Описание
Вместо вывода на экран совпавших с шаблоном строк выводит их количество
-E Включает расширенные регулярные выражения
-h Ужимает обычное начало каждой строки вывода за счет удаления имени файла, в котором строка найдена
-i Не учитывает регистр букв
-l Перечисляет имена файлов со строками, совпадающими с шаблоном; не выводит сами найденные строки
-v Меняет шаблон соответствия для выбора вместо строк, соответствующих шаблону, несовпадающих с ним строк

Выполните упражнение 2.17.

Упражнение 2.17. Основной вариант использования команды grep

Посмотрим команду grep в действии на примерах простых шаблонов.

$ grep in words.txt

When shall we three meet again. In thunder, lightning, or in rain?

I come, Graymalkin!

$ grep -c in words.txt words2.txt

words.txt:2 words2.txt:14

$ grep -c -v in words.txt words2.txt

words.txt:9

words2.txt:16$

Как это работает

В первом примере нет опций; в нем просто ищется строка in в файле words.txt и выводятся на экран любые строки, соответствующие условию поиска. Имя файла не отображается, поскольку поиск велся в единственном файле.

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

В заключение применяется опция -v для инвертирования критерия поиска и подсчета строк, не совпадающих с шаблоном.

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

Как вы убедились, основной вариант применения команды grep легко усвоить. Теперь пришло время рассмотреть основы построения регулярных выражений, которые позволят вам выполнять более сложный поиск. Как упоминалось ранее в этой главе, регулярные выражения применяются в системе Linux и многих языках программирования с открытым исходным кодом. Вы можете использовать их в редакторе vi и в скриптах на языке Perl, применяя одни и те же принципы, общие для регулярных выражений, где бы они ни появлялись.

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

Таблица 2.15

Символ Описание
^ Привязка к началу строки
$ Привязка к концу строки
. Любой одиночный символ
[] В квадратных скобках содержится диапазон символов, с любым из них возможно совпадение, например, диапазон символов a-e или инвертированный диапазон, перед которым стоит символ ^

Если вы хотите использовать любые из перечисленных символов как "обычные", поставьте перед ними символ \. Например, если нужно найти символ $, просто введите \$.

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

Таблица 2.16

Проверочный шаблон Описание
[:alnum:] Буквенно-цифровые символы
[:alpha:] Буквы
[:ascii:] Символы таблицы ASCII
[:blank:] Пробел или табуляция
[:cntrl:] Управляющие символы ASCII
[:digit:] Цифры
[:graph:] Неуправляющие и непробельные символы
[:lower:] Строчные буквы
[:print:] Печатные символы
[:punct:] Знаки пунктуации
[:space:] Пробельные символы, включая вертикальную табуляцию
[:upper:] Прописные буквы
[:xdigit:] Шестнадцатиричные цифры

Кроме того, если задана опция =E для расширенного соответствия, за регулярным выражением могут следовать и другие символы, управляющие выполнением проверки на соответствие шаблону (табл. 2.17). В команде grep перед этими символами необходимо вводить символ \.

Таблица 2.17

Опция  Описание
? Совпадение не обязательно, но возможно не более одного раза
* Совпадения может не быть, оно может быть однократным или многократным
+ Совпадение должно быть однократным или многократным
{n} Совпадение должно быть n раз
{n, } Совпадение должно быть n раз и больше
{n, m} Совпадение должно быть от n до m раз включительно

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

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

$ grep e$ words2.txt

Art thou not, fatal vision, sensible

I see thee yet, in form as palpable

Nature seems dead, and wicked dreams abuse

$

Как видите, найдены строки, заканчивающиеся буквой "е".

2. Теперь найдите трехбуквенные слова, начинающиеся с символов "Th". В данном случае вам понадобится шаблон [[:space:]] для ограничения длины слова и . для единственного дополнительного символа.

$ grep Th.[[:space:]] words 2.txt

The handle toward my hand? Come, let me clutch thee.

The curtain'd sleep; witchcraft celebrates

Thy very stones prate of my whereabout,

$

3. В заключение примените расширенный режим поиска в команде grep для обнаружения слов из строчных букв длиной ровно 10 символов. Для этого задайте диапазон совпадающих символов от а до z и 10 повторяющихся совпадений.

$ grep -Е [a-z]\{10\} words2.txt

Proceeding from the heat-oppressed brain?

And such an instrument I was to use.

The curtain'd sleep; witchcraft celebrates

hy very stones prate of my whereabout,

$

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

Выполнение команд

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

Сделать это можно с помощью синтаксической конструкции $(команда), показанной ранее в примере с командой set. Существует устаревший вариант подстановки команды `команда`, который все еще широко распространен.

Примечание

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

Во всех современных сценариях следует применять конструкцию выполнения или подстановки команды $(команда), которая введена для того, чтобы избавиться от довольно сложных правил использования символов $' и \ внутри команды, заключенной в обратные апострофы. Если применяется обратный апостроф внутри конструкции `...` , его необходимо экранировать символом \. Эти непонятные знаки часто заставляют программистов путаться, и иногда даже опытные специалисты в программировании средствами командной оболочки вынуждены ставить опыты для того, чтобы добиться правильного использования кавычек и апострофов в командах, заключенных в обратные апострофы.

Результат выполнения конструкции $(команда) — просто вывод команды. Имейте в виду, что это не статус возврата команды, а просто строковый вывод, показанный далее.

#!/bin/sh

echo The current directory is $PWD

echo The current users are $(who)

exit 0

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

Если вы хотите поместить результат в переменную, то можете просто присвоить его обычным образом:

whoisthere=$(who)

echo Swhoisthere

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

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

Подстановки в арифметических выражениях

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

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

#!/bin/sh

х=0

while [ "$х" -ne 10 ]; do

 echo $х

 х=$(($x+1))

done

exit 0

Примечание

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

Подстановка значений параметров

Вы уже видели простейший вариант присваивания параметра и подстановки значения параметра:

foo=fredecho $foo

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

#!/bin/sh

for i in 1 2 do

 my_secret_process $i_tmp

done

Но в каждом проходе цикла вы получите следующее сообщение:

my_secret_process: too few arguments

В чем ошибка?

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

#!/bin/sh

for i in 1 2 do

 my_secret_process ${i}_tmp

done

В каждом проходе цикла вместо ${i} подставляется значение i и получаются реальные имена файлов. Вы подставляете значение параметра в строку.

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

Таблица 2.18

Шаблон подстановки параметра Описание
${парам:-значение по умолчанию} Если у парам нет значения, ему присваивается значение по умолчанию
${#парам} Задается длина парам
${парам%строка} От конца значения парам отбрасывается наименьшая порция, совпадающая со строкой, и возвращается остальная часть значения
${парам%%строка} От конца значения парам отбрасывается наибольшая порция, совпадающая со строкой, и возвращается остальная часть значения
${парам#строка} От начала значения парам отбрасывается наименьшая порция, совпадающая со строкой, и возвращается остальная часть значения
${парам##строка} От начала значения парам отбрасывается наибольшая порция, совпадающая со строкой, и возвращается остальная часть значения

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

В приведенном далее сценарии показано применение шаблонов при подстановках значений параметров.

#!/bin/sh

unset foo

echo ${foo:-bar}

foo=fud

echo ${foo:-bar}

foo=/usr/bin/X11/startx

echo ${foo#*/}

echo ${foo##*/}

bar=/usr/local/etc/local/networks

echo ${bar%local*}

echo ${bar%%local*}

exit 0

У этого сценария следующий вывод:

bar

fud

usr/bin/X11/startx

startx

/usr/local/etc/usr

Как это работает

Первая подстановка ${foo:-bar} дает значение bar, поскольку у foo нет значения в момент выполнения команды. Переменная foo остается неизменной, т.е. она остается незаданной.

Примечание

Подстановка ${foo:=bar} установила бы значение переменной $foo. Этот строковый шаблон устанавливает, что переменная foo существует и не равна null. Если значение переменной не равно null, оператор возвращает ее значение, в противном случае вместо этого переменной foo присваивается значение bar.

Подстановка ${foo:?bar} выведет на экран foo: bar и аварийно завершит команду, если переменной foo не существует или ее значение не определено. И наконец, ${foo:+bar} вернет bar, если foo существует и не равна null. Какое разнообразие вариантов!

Шаблон {foo#*/} задает поиск и удаление только левого символа / (символ * соответствует любой строке, в том числе и пустой). Шаблон {foo##*/} задает поиск максимальной подстроки, совпадающей с ним, и, таким образом, удаляет самый правый символ / и все предшествующие ему символы.

Шаблон ${bar%local*} определяет просмотр символов в значении параметра, начиная от крайнего правого, до первого появления подстроки local, за которой следует любое количество символов, а в случае шаблона ${bar%%local*} ищется максимально возможное количество символов, начиная от крайнего правого символа значения и заканчивая крайним левым появлением подстроки local.

Поскольку в системах UNIX и Linux многое основано на идеи фильтров, результат какой-либо операции часто должен перенаправляться вручную. Допустим, вы хотите преобразовать файлы GIF в файлы JPEG с помощью программы cjpeg:

$ cjpeg image.gif > image.jpg

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

#!/bin/sh

for image in *.gif

do

 cjpeg $image > {image%%gif}jpg

done

Этот сценарий, giftojpeg, создает в текущем каталоге для каждого файла формата GIF файл формата JPEG.

Встроенные документы

Особый способ передачи из сценария командной оболочки входных данных команде — использование встроенного документа (here document). Такой документ позволяет команде выполняться так, как будто она читает данные из файла или с клавиатуры, в то время как на самом деле она получает их из сценария.

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

Рассмотрим упражнение 2.19.

Упражнение 2.19. Применение встроенных документов

Простейший пример просто передает входные данные команде cat.

#!/bin/sh

cat <<!FUNKY!

hello

this is a here

document

!FUNKY!

Этот пример выводит на экран следующие строки:

hello

this is a here

document

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

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

Упражнение 2.20. Ещё одно применение встроенного документа

1. Начнем с файла, названного a_text_file и содержащего следующие строки:

That is line 1

That is line 2

That is line 3That is line 4

2. Вы можете отредактировать этот файл, совместно используя встроенный документ и редактор ed:

#!/bin/sh

ed a_text_file <<!FunkyStuff!

3

d

., \$s/is/was/ w

q

!FunkyStuff!

exit 0

Если вы выполните этот сценарий, то увидите, что теперь файл содержит следующие строки:

That is line 1

That is line 2

That was line 4

Как это работает

Сценарий командной оболочки запускает редактор ed и передает ему команды, необходимые для перехода к третьей строке, удаления строки и затем замены ее содержимым текущей строки (поскольку строка 3 (line 3) была удалена, теперь текущая строка — последняя строка файла). Эти команды редактора ed берутся из строк сценария, формирующих встроенный документ, строк между маркерами !Funky Stuff!.

Примечание

Обратите внимание на знак \ внутри встроенного документа, применяемый для защиты от подстановки, выполняемой командной оболочкой. Символ \ экранирует знак $, поэтому оболочка знает, что не следует пытаться подставить вместо строки \$s/is/was/ ее значение, которого у нее конечно же нет. Оболочка просто передает текст \$ как $, который затем сможет интерпретировать редактор e

Отладка сценариев

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

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

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

Таблица 2.19

Опция командной строки Опция команды set Описание
sh -n <сценарий> set -о noexec  set -n Только проверяет синтаксические ошибки; не выполняет команды
sh -v <сценарий> set -о verbose  set -v Выводит на экран команды перед их выполнением
sh -х <сценарий> set -о xtrace  set -x Выводит на экран команды после обработки командной строки
sh -u <сценарий> set -o nounset  set -u Выдает сообщение об ошибке при использовании неопределенной переменной

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

Для установки опции xtrace используйте следующую команду:

set -о xtrace

Для того чтобы снова отключить эту опцию, применяйте следующую команду:

set +о xtrace

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

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

trap 'echo Exiting: critical variable = $critical_variable' EXIT

По направлению к графическому режиму — утилита dialog

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

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

Примечание

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

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

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

dialog --msgbox "Hello World" 9 18

На экране появится графическое информационное окно, дополненное кнопкой OK (рис. 2.3).

Рис. 2.3

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

Таблица 2.20

Тип диалогового окна Опция, применяемая для создания окна этого типа Назначение окна
Окна с флажками (Check boxes) --checklist Позволяет отображать список флажков, каждый из которых можно установить или сбросить
Информационные окна (Info boxes) --infobox Простое немедленное отображение в окне, без очистки экрана, возвращаемых данных
Окна ввода (Input boxes) --inputbox Позволяет пользователю вводить в окно текст
Окна меню (Menu boxes) --menu Позволяет пользователю выбрать один пункт из списка
Окна сообщений (Message boxes) --msgbox Отображает сообщения для пользователей и снабжено кнопкой OK, которую они должны нажать для продолжения
Окна с переключателями (Radio selection boxes) --radiolist Позволяет пользователю выбрать один переключатель из списка
Текстовые окна (Text boxes) --textbox Позволяют отображать содержимое файла в окне с прокруткой
Диалоговые окна Да/Нет (Yes/No boxes) --yesno Позволяют задать вопрос, на который пользователь может ответить "Да" или "Нет"

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

Для получения вывода из диалогового окна любого типа, допускающего текстовый ввод или выбор, вы должны перехватить стандартный поток ошибок, как правило, направляя его во временный файл, который вы сможете обработать позже. Для получения ответа на вопросы типа "Да"/"Нет", просто проверьте код завершения, который, как и во всех соблюдающих приличия программах, в случае успеха возвращает 0 (т. е. выбор ответа "Да" (Yes)) и 1 в остальных случаях.

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

Таблица 2.21

Тип диалогового окна Параметры
--checklist text height width list-height [tag text status] ...
--infobox text height width
--inputbox text height width [initial string]
--menu text height width menu-height [tag item ] ...
--msgbox text height width
--radiolist text height width list-height [tag text status] ...
--textbox filename height width
--yesno text height width

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

Выполните упражнения 2.21 и 2.22.

Упражнение 2.21. Применение утилиты dialog

Давайте сразу перейдем к красивому сложному примеру. Если вы поймете его, все остальные покажутся легкими! В этом примере вы создадите диалоговое окно со списком флажков, с заголовком Check me (Поставь галочку) и пояснительной надписью Pick Numbers (Выбери номера). Окно с флажками будет высотой 15 строк и шириной 25 символов, и каждый флажок будет занимать 3 символа по высоте. И последнее, но не по степени важности, вы перечислите отображаемые элементы вместе с принятой по умолчанию установкой или сбросом (on/off) флажка.

dialog --title "Check me" --checklist "Pick Numbers" 15 25 3 1 "one" "off" 2 "two" "on" 3 "three" "off"

Полученный результат показан на рис. 2.4.

Как это работает

В этом примере параметр --checklist указывает на то, что вы собираетесь создать диалоговое окно с флажками. Вы используете опцию --title для задания заголовка "Check me", следующий параметр — пояснительная надпись "Pick Numbers".

Далее вы переходите к указанию размеров диалогового окна. Оно будет высотой 15 строк и шириной 25 символов и 3 строки отводятся для меню. Это не самый удачный выбор размеров, но он позволит вам увидеть, как размещаются элементы.

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

□ номер в списке;

□ текст;

□ состояние.

Рис. 2.4

У первого элемента номер 1, отображается текст "one" (один) и выбрано состояние "off" (сброшен). Далее вы переходите ко второму элементу с номером 2, текстом "two" и состоянием "on" (установлен). Так продолжается до тех пор, пока вы не опишите все элементы списка.

Легко, не правда ли? Теперь попробуйте ввести несколько вариантов в командной строке и убедитесь, насколько эту утилиту легко применять. Для того чтобы включить этот пример в программу, вы должны иметь доступ к результатам пользовательского ввода. Это совсем просто: перенаправьте стандартный поток ошибок в текстовый ввод или проверьте переменную окружения $?, которая, как вы помните, не что иное, как код завершения предыдущей команды.

Упражнение 2.22. Более сложная программа, использующая утилиту dialog

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

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

#!/bin/sh

# Задайте несколько вопросов и получите ответ

dialog --title "Questionnaire" --msgbox "Welcome to my simple survey" 9 18

2. Спросите пользователя с помощью простого диалогового окна с кнопками типа Yes/No, хочет ли он продолжать. Воспользуйтесь переменной окружения $? для того, чтобы выяснить, выбрал пользователь ответ Yes (код завершения 0) или No. Если он не хочет двигаться дальше, используйте простое информационное окно, не требующее никакого пользовательского ввода для своего завершения.

dialog --title "Confirm" --yesno "Are you willing to take part?" 9 18

if [ $? != 0 ]; then

 dialog --infobox "Thank you anyway" 5 20 sleep 2

 dialog --clear exit 0

fi

3. Спросите у пользователя его имя с помощью диалогового окна ввода. Перенаправьте стандартный поток ошибок во временный файл _1.txt, который затем вы сможете обработать в переменной QNAME.

dialog --title "Questionnaire" --inputbox "Please enter your name" 9 30 2>_1.txt

Q_NAME=$(cat _1.txt)

4. Здесь у вас появляется меню из четырех пунктов. И снова вы перенаправляете стандартный поток ошибок и загружаете его в переменную.

dialog --menu "$Q_NAME, what music do you like best?" 15 30 4 1 "Classical" 2 "Jazz" 3 "Country" 4 "Other" 2>_1.txt

Q_MUSIC=$(cat _1.txt)

5. Номер, выбранный пользователем, будет запоминаться во временном файле _1.txt, который перехватывается переменной Q_MUSIC, поэтому вы сможете проверить результат.

if [ "$Q_MUSIC" = "1" ]; then

 dialog --title "Likes Classical" --msgbox "Good choice!" 12 25

else

 dialog --title "Doesn't like Classical" --msgbox "Shame" 12 25

fi

В заключение очистите последнее диалоговое окно и завершите программу.

sleep 2

dialog --clear

exit 0

На рис. 2.5 показан результат.

Как это работает

В данном примере вы соединяете команду dialog и простой программный код на языке командной оболочки для того, чтобы показать, как можно создавать простые программы с графическим пользовательским интерфейсом, используя только сценарий командной оболочки. Вы начинаете с обычного экрана-приветствия, а затем с помощью простого диалогового окна с кнопками типа Yes/No спрашиваете пользователя о его желании участвовать в опросе. Вы используете переменную $? для проверки ответа пользователя. Если он согласен, вы запрашиваете его имя, сохраняете его в переменной Q_NAME и выясняете с помощью диалогового окна-меню, какой музыкальный стиль он любит. Сохранив числовой вывод в переменной Q_MUSIC, вы сможете увидеть, что ответил пользователь, и отреагировать соответственно.

Рис. 2.5

Рис. 2.6

Если вы применяете графический пользовательский интерфейс (GUI) на базе графической среды GNOME и в данный момент запустили в нем сеанс работы с терминалом, на месте команды dialog можно использовать команду gdialog. У обеих команд одинаковые параметры, поэтому вы сможете воспользоваться тем же программным кодом, не считая замены запускаемой вами команды dialog командой gdialog. На рис. 2.6 показано, как выглядит этот сценарий в дистрибутиве Ubuntu, когда применяется команда gdialog.

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

Соединяем все вместе

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

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

Требования

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

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

Проектирование

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

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

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

Если вы допускаете, что на компакт-диске может быть разное количество дорожек, у вас есть три варианта:

□ использовать один файл с одной строкой для "заголовочной" типовой информации и n строк для сведений о дорожках на каждом компакт-диске;

□ поместить всю информацию о каждом компакт-диске в одну строку, разрешая ей продолжаться то тех пор, пока вся информация о дорожках диска не будет сохранена;

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

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

Далее нужно решить, какие данные помещать в файлы.

Сначала вы выбираете для заголовка каждого компакт-диска хранение следующей информации:

□ номер компакт-диска в каталоге;

□ название;

□ музыкальный стиль (классика, рок, поп, джаз и т.д.);

□ композитор или исполнитель.

О дорожках вы будете хранить две характеристики:

□ номер дорожки;

□ ее название.

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

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

Таблица 2.22

Catalog Title Type Composer
CD123 Cool sax Jazz Bix
CD234 Classic violin Classical Bach
CD345 Hits99 Pop Various

Таблица 2.23

Catalog Track No. Title
CD123 1 Some jazz
CD123 2 More jazz
CD234 1 Sonata in D minor
CD345 1 Dizzy

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

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

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

get_return();

get_confirm();

set_menu_choice();

insert_title();

insert_track();

add_record_tracks();

add_records();

find_cd();

update_cd();

count_cds();

remove_records();

list_tracks().

Упражнение 2.23. Приложение для работы с коллекцией компакт-дисков

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

#!/bin/bash

# Очень простой пример сценария командной оболочки для управления

# коллекцией компакт-дисков.

# Copyright (С) 1996-2007 Wiley Publishing Inc.

# Это свободно распространяемое программное обеспечение;

# вы можете распространять эту программу и/или изменять ее

# в соответствии с положениями GNU General Public License,

# документа, опубликованного фондом Free Software Foundation;

# либо версии 2 этой лицензии или (по вашему выбору)

# любой более свежей версии.

# Эта программа распространяется в надежде на ее полезность,

# но WITHOUT ANY WARRANTY, (без каких-либо гарантий);

# даже без предполагаемой гарантии MERCHANTABILITY

# or FITNESS FOR A PARTICULAR PURPOSE (годности

# ее для продажи или применения для определенной цели).

# Более подробную информацию см. в GNU General Public License.

# Вы должны были получить копию GNU General Public License

# вместе с этой программой;

# если нет, пишите в организацию Free Software Foundation,

# Inc. no адресу: 675 Mass Ave, Cambridge, MA 02139, USA.

2. Теперь убедитесь, что установлены некоторые глобальные переменные, которые будут использоваться во всем сценарии. Задайте заголовочный файл, файл с данными о дорожках и временный файл и перехватите нажатие комбинации клавиш <Ctrl>+<C> для того, чтобы удалить временный файл, если пользователь прервет выполнение сценария.

menu_choice=""

current cd=""

title_file="title.cdb"

tracks_file="tracks.cdb"

temp_file=/tmp/cdb.$$

trap 'rm -f $temp_file' EXIT

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

get_return() (

 echo -е "Press return \с"

 read x

 return 0

}

get_confirm() (

 echo -e "Are you sure? \c"

 while true do

  read x

  case "$x" in

   y | yes | Y | Yes | YES )

    return 0;;

   n | no | N | No | NO )

    echo

    echo "Cancelled"

    return 1;;

   *)

    echo "Please enter yes or no" ;;

  esac

 done

}

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

set_menu_choice() {

 clear

 echo "Options :-"

 echo

 echo " a) Add new CD"

 echo " f) Find CD"

 echo " c) Count the CDs and tracks in the catalog"

 if [ "$cdcatnum" != "" ]; then

  echo " 1) List tracks on $cdtitle"

  echo " r) Remove $cdtitle"

  echo " u) Update track information for $cdtitle"

 fi

 echo " q) Quit" echo

 echo -e "Please enter choice then press return \c"

 read menu_choice

 return

}

Примечание

Имейте в виду, что команда echo -е не переносится в некоторые командные оболочки.

5. Далее идут две очень короткие функции, insert_title и insert_track, для пополнения файлов базы данных. Несмотря на то, что некоторые программисты ненавидят однострочные функции вроде этих, они помогают сделать понятнее другие функции.

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

insert_title() {

 echo $* >> $title_file

 return

}

insert_track() {

 echo $* >> $tracks_file

 return

}

add_record_tracks() {

 echo "Enter track information for this CD"

 echo "When no more tracks enter q"

 cdtrack=1

 cdttitle=""

 while [ "$cdttitle" != "q" ]

 do

  echo -e "Track $cdtrack, track title? \c"

  read tmp

  cdttitle=${tmp%%, *}

  if [ "$tmp" != "$cdttitle" ]; then

   echo "Sorry, no commas allowed"

   continue

  fi

  if [ -n "$cdttitle" ] ; then

   if [ "$cdttitle" ! = "q" ]; then

    insert_track $cdcatnum, $cdtrack, $cdttitle

   fi

  else

   cdtrack=$((cdtrack-1))

  fi

  cdtrack=$((cdtrack+1))

 done

}

6. Функция add_records позволяет вводить основную информацию о новом компакт-диске.

add_records() {

 # Подсказка для начала ввода информации

 echo -е "Enter catalog name \с"

 read tmp

 cdcatnum=${tmp%%, *}

 echo -e "Enter title \c"

 read tmp

 cdtitle=${tmp%%, *}

 echo -e "Enter type \c"

 read tmp

 cdtype=${tmp%%, *}

 echo -e "Enter artist/composer \c"

 read tmp

 cdac=${tmp%%, *}

 # Проверяет, хочет ли пользователь ввести информацию

 echo About to add new entry

 echo "$cdcatnum $cdtitle $cdtype $cdac"

 # Если получено подтверждение, добавляет данные в конец файла.

 # с заголовками

 if get_confirm ; then

  insert_title $cdcatnum, $cdtitle, $cdtype, $cdac

  add_record_tracks

 else

  remove_records

 fi

 return

}

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

У команды счетчика слов, wc, в выводе есть пробельный символ, разделяющий количества строк, слов и символов в файле. Используйте синтаксическую запись $(wc -l $temp_file) для извлечения первого параметра в выводе и переноса его в переменную linesfound. Если бы вам был нужен другой следующий далее параметр, нужно было бы воспользоваться командой set для установки значений переменных-параметров оболочки из вывода команды.

Изменив значение переменной IFS (Internal Field Separator, внутренний разделитель полей) на запятую, вы сможете разделить поля, разграниченные запятыми. Альтернативный вариант — применить команду cut.

find_сd() {

 if [ "$1" = "n" ]; then

  asklist=n

 else

  asklist=y

 fi

 cdcatnum=""

 echo -e "Enter a string to search for in the CD titles \c"

 read searchstr

 if [ "$searchstr" = "" ]; then

  return 0

 fi

 grep "$searchstr" $title_file > $temp_file

 set $(wc -l $temp_file)

 linesfound=$1

 case "$linesfound" in

  0)

   echo "Sorry, nothing found"

   get_return

   return 0 ;;

  1) ;;

  2)

   echo "Sorry, not unique."

   echo "Found the following"

   cat $temp_file

   get_return

   return 0

 esac

 IFS=", "

 read cdcatnum cdtitle cdtype cdac < $temp_file

 IFS=" "

 if [ -z "$cdcatnum" ]; then

  echo "Sorry, could not extract catalog field from $temp_file"

  get_return

  return 0

 fi

 echo

 echo Catalog number: $cdcatnum echo Title: $cdtitle

 echo Type: $cdtype

 echo Artist/Composer: $cdac

 echo

 get_return

 if [ "$asklist" = "y" ]; then

  echo -e "View tracks for this CD? \c"

  read x

  if [ "$x" = "y" ]; then

   echo

   list_tracks

   echo

  fi

 fi

 return 1

}

8. Функция update_cd позволит вам повторно ввести сведения о компакт-диске. Учтите, что вы ищите (с помощью команды grep) строки, начинающиеся (^) с подстроки $cdcatnum, за которой следует ", " и должны заключить подстановку значения $cdcatnum в {}. Таким образом, вы сможете найти запятую без специального пробельного символа между ней и номером в каталоге. Эта функция также использует {} для образования блока из нескольких операторов, которые должны выполняться, если функция get_confirm вернет значение true.

update_cd() {

 if [ -z "$cdcatnum" ]; then

  echo "You must select a CD first"

  find_cd n

 fi

 if [ -n "$cdcatnum" ]; then

  echo "Current tracks are :-"

  list_tracks

  echo

  echo "This will re-enter the tracks for $cdtitle"

  get_confirm && {

   grep -v "^${cdcatnum}, " $tracks_file > $temp_file

   mv $temp_file $tracks_file

   echo

   add_record_tracks

  }

 fi

 return

}

9. Функция count_cds дает возможность быстро пересчитать содержимое базы данных.

count_cds() {

 set $(wc -l $title_file)

 num_titles=$1

 set $(wc -l $tracks_file)

 num_tracks=$1

 echo found $num_titles CDs, with a total of $num_tracks tracks

 get_return

 return

}

10. Функция remove_records удаляет элементы из файлов базы данных с помощью команды grep -v, удаляющей все совпадающие строки. Учтите, что нужно применять временный файл.

Если вы попытаетесь применить команду:

grep -v "^$cdcatnum" > $title_file

файл $title_file станет пустым благодаря перенаправлению вывода > до того, как команда grep выполнится, поэтому она будет читать уже пустой файл.

remove_records() {

 if [ -z "$cdcatnum" ]; then

  echo You must select a CD first find_cd n

 fi

 if [ -n "$cdcatnum" ]; then

  echo "You are about to delete $cdtitle"

  get_confirm && {

   grep -v "^${cdcatnum}, " $title_file > $temp_file

   mv $temp_file $title_file

   grep -v "^${cdcatnum}, " $tracks_file > $temp_file

   mv $temp_file $tracks_file

   cdcatnum=""

   echo Entry removed

  }

  get_return

 fi

 return

}

11. Функция list_tracks снова использует команду grep для извлечения нужных вам строк, команду cut для доступа к отдельным полям и затем команду more для постраничного вывода. Если вы посмотрите, сколько строк на языке С займет повторная реализация этих 20 необычных строк кода, то поймете, каким мощным средством может быть командная оболочка.

list_tracks() {

 if [ "$cdcatnum" = "" ]; then

  echo no CD selected yet

  return

 else

  grep "^${cdcatnum}, " $tracks_file > $temp_file

  num_tracks=${wc -l $temp_file}

  if [ "$num_tracks" = "0" ]; then

   echo no tracks found for $cdtitle

  else

   {

    echo

    echo "$cdtitle :-"

    echo

    cut -f 2- -d , $temp_file

    echo

   } | ${PAGER:-more}

  fi

 fi

 get_return

 return

}

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

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

rm -f $temp_file

if [ ! -f $title_file ]; then

 touch $title_file

fi

if [ ! -f $tracks_file ]; then

 touch $tracks_file

fi

# Теперь непосредственно приложение

clear

echo

echo

echo "Mini CD manager" sleep 1

quit=n

while [ "$quit" != "y" ]; do

 set_menu_choice

 case "$menu_choice" in

  a) add_records;;

  r) remove records;;

  f) find_cd y;;

  u) update_cd;;

  c) count_cds;;

  l) list_tracks;;

  b)

   echo

   more $title_file

   echo

   get return;;

  q | Q ) quit=y;;

  *) echo "Sorry, choice not recognized";;

 esac

done

# Убираем и покидаем

rm -f $temp_file echo "Finished"

exit 0

Замечания, касающиеся приложения

Команда trap в начале сценария предназначена для перехвата нажатия пользователем комбинации клавиш <Ctrt>+<C>. Им может быть сигнал EXIT или INT, в зависимости от настроек терминала.

Существуют другие способы реализации выбора пункта меню, особенно конструкция select в оболочках bash и ksh (которая, тем не менее, не определена в стандарте X/Open). Она представляет собой специализированный селектор пунктов меню. Проверьте ее на практике, если ваш сценарий может позволить себе быть немного менее переносимым. Для передачи пользователям многострочной информации можно также воспользоваться встроенными документами.

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

1 First CD Track 1

2 First CD Track 2

1 Another CD

2 With the same CD key

Мы оставляем это и другие усовершенствования в расчете на ваше воображение и творческие способности, которые проявятся при корректировке вами программного кода в соответствии с требованиями GPL.

Резюме 

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

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

Глава 3

Работа с файлами

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

Прежде чем перейти к способам обработки файлового ввода/вывода в системе Linux, мы дадим краткий обзор понятий, связанных с файлами, каталогами и устройствами. Для управления файлами и каталогами вам придется выполнять системные вызовы (аналог Windows API в системах UNIX и Linux), но, кроме того, для обеспечения более эффективного управления файлами существует большой набор библиотечных функций стандартной библиотеки ввода/вывода (stdio).

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

□ файлы и устройства;

□ системные вызовы;

□ библиотечные функции;

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

□ управление файлами;

□ стандартная библиотека ввода/вывода;

□ форматированный ввод и вывод;

□ сопровождение файлов и каталогов;

□ просмотр каталогов;

□ ошибки;

□ файловая система /proc;

□ более сложные приемы — fcntl и mmap.

Структура файла в Linux

Вы можете спросить: "Зачем вы останавливаетесь на структуре файла? Я уже знаком с ней." Дело в том, что в среде Linux, как и UNIX, файлы особенно важны, поскольку они обеспечивают простую и согласованную взаимосвязь со службами операционной системы и устройствами. В ОС Linux файл — это все что угодно. Ну, или почти все!

Это означает, что в основном программы могут обрабатывать дисковые файлы, последовательные порты, принтеры и другие устройства точно так же, как они используют файлы. Мы расскажем о некоторых исключениях, таких как сетевые подключения, в главе 15, но в основном вы должны будете применять пять базовых функций: open, close, read, write и ioctl.

Каталоги — тоже специальный тип файлов. В современных версиях UNIX, включая Linux, даже суперпользователь не пишет непосредственно в них. Обычно все пользователи для чтения каталогов применяют интерфейс opendir/readdir, и им нет нужды знать подробности реализации каталогов в системе. Позже в этой главе мы вернемся к специальным функциям работы с каталогами.

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

Каталоги

Помимо содержимого у файла есть имя и набор свойств, или "административная информация", т.е. дата создания/модификации файла и права доступа к нему. Свойства хранятся в файловом индексе (inode), специальном блоке данных файловой системы, который также содержит сведения о длине файла и месте хранения файла на диске. Система использует номер файлового индекса; для нашего удобства структуру каталога также называют файлом.

Каталог — это файл, содержащий номера индексов и имена других файлов. Каждый элемент каталога — ссылка на файловый индекс; удаляя имя файла, вы удаляете ссылку. (Номер индекса файла можно увидеть с помощью команды ln -i.) Применяя команду ln, вы можете создать ссылки на один и тот же файл в разных каталогах.

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

Файлы помещаются в каталоги, которые могут содержать подкаталоги. Так формируется хорошо знакомая иерархия файловой системы. Пользователь, скажем neil, обычно хранит файлы в исходном (home) каталоге, возможно /home/neil, с подкаталогами для хранения электронной почты, деловых писем, служебных программ и т.д. Имейте в виду, что у многих командных оболочек систем UNIX и Linux есть отличное обозначение для указания начала пути в вашем исходном каталоге: символ "тильда" (~). Для другого пользователя наберите ~user. Как вы знаете, исходные каталоги пользователей — это, как правило, подкаталоги каталога более высокого уровня, создаваемого специально для этой цели, в нашем случае это каталог /home.

Примечание

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

Каталог /home в свою очередь является подкаталогом корневого каталога /, расположенного на верхнем уровне иерархии и содержащего все системные файлы и подкаталоги. В корневой каталог обычно включен каталог /bin для хранения системных программ (бинарных файлов), каталог /etc, предназначенный для хранения системных файлов конфигурации, и каталог /lib для хранения системных библиотек. Файлы, представляющие физические устройства и предоставляющие интерфейс для этих устройств, принято помещать в каталог /dev. На рис. 3.1 показана в качестве примера часть типичной файловой системы Linux. Мы рассмотрим структуру файловой системы Linux более подробно в главе 18, когда будем обсуждать стандарт файловой системы Linux (Linux File System Standard).

Рис. 3.1 

Файлы и устройства

Даже физические устройства очень часто представляют (отображают) с помощью файлов. Например, будучи суперпользователем, вы можете смонтировать дисковод IDE CD-ROM как файл:

# mount -t iso9660 /dev/hdc /mnt/cdrom

# cd /mnt/cdrom

который выбирает устройство CD-ROM (в данном случае вторичное ведущее (secondary master) устройство IDE, которое загружается как /dev/hdc во время начального запуска системы; у устройств других типов будут другие элементы каталога /dev) и монтирует его текущее содержимое как файловую структуру в каталоге /mnt/cdrom. Затем вы перемещаетесь по каталогам компакт-диска как обычно, конечно за исключением того, что их содержимое доступно только для чтения.

В системах UNIX и Linux есть три важных файла устройств: /dev/console, /dev/tty и /dev/null.

dev/console

Это устройство представляет системную консоль. На него часто отправляются сообщения об ошибках и диагностическая информация. У всех систем UNIX есть выделенный терминал или экран для получения сообщений консоли. Иногда он может быть выделенным печатающим терминалом. На современных рабочих станциях и в ОС Linux обычно это активная виртуальная консоль, а под управлением графической среды X Window это устройство станет специальным окном консоли на экране.

/dev/tty

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

Там где этот файл, /dev/tty может применяться, он позволяет программе писать непосредственно пользователю независимо от того, какой псевдотерминал или аппаратный терминал он использует. Это полезно при перенаправлении стандартного вывода. Примером может служить отображение содержимого длинного каталога в виде группы страниц с помощью команды ls -R | more, в которой у программы more есть пользовательская подсказка для каждой новой страницы вывода. Вы узнаете больше о файле /dev/tty в главе 5.

Учтите, что существует только одно устройство /dev/console, и в то же время может существовать много разных физических устройств, к которым можно обратиться с помощью файла dev/tty.

/dev/null

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

$ echo do not want to see this >/dev/null

$ cp /dev/null empty_file

Примечание

Другой способ создания пустых файлов — применение команды touch <имя файла>, изменяющей время модификации файла или создающей новый файл при отсутствии файла с заданным именем. Хотя она и не очищает содержимое обрабатываемого файла.

В каталоге /dev можно найти и другие устройства, такие как дисководы жестких дисков и флоппи-дисководы, коммуникационные порты, ленточные накопители, дисководы CD-ROM, звуковые карты и некоторые устройства, представляющие внутреннюю структуру системы. Есть даже устройство /dev/zero, действующее как источник нулевых байтов для создания файлов, заполненных нулями. Для доступа к некоторым из этих устройств вам понадобятся права супер пользователя; обычные пользователи не могут писать программы, непосредственно обращающиеся к низкоуровневым устройствам, таким как накопители жестких дисков. Имена файлов устройств могут быть в разных системах различными. В дистрибутивах ОС Linux обычно есть приложения, выполняемые от имени суперпользователя и управляющие устройствами, которые иначе будут недоступны, например, mount для монтируемых пользователями файловых систем.

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

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

Системные вызовы и драйверы устройств

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

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

Для формирования одинакового интерфейса драйверы устройств включают в себя все аппаратно-зависимые свойства. Уникальные аппаратные средства обычно доступны через системный вызов ioctl (I/O control, управление вводом/выводом).

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

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

open — открывает файл или устройство;

read — читает из открытого файла или устройства;

□ write — пишет в файл или устройство;

close — закрывает файл или устройство;

ioctl — передает управляющую информацию драйверу устройства.

Системный вызов ioctl применяется для аппаратно-зависимого управления (как альтернатива стандартного ввода/вывода), поэтому он у каждого устройства свой. Например, вызов ioctl может применяться для перемотки ленты в ленточном накопителе или установки характеристик управления потоками последовательного порта. Этим объясняется необязательная переносимость ioctl с машины на машину. Кроме того, у каждого драйвера определен собственный набор команд ioctl.

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

Библиотечные функции

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

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

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

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

Библиотечные функции, как правило, описываются в разделе 3 интерактивного справочного руководства и часто снабжаются стандартным файлом директивы include, связанным с ними, например, файл stdio.h для стандартной библиотеки ввода/вывода.

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

Рис. 3.2

Низкоуровневый доступ к файлам

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

□ 0 — стандартный ввод;

□ 1 — стандартный вывод;

□ 2 — стандартный поток ошибок.

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

write

Системный вызов write предназначен для записи из buf первых nbytes байтов в файл, ассоциированный с дескриптором fildes. Он возвращает количество реально записанных байтов, которое может быть меньше nbytes, если в дескрипторе файла обнаружена ошибка или дескриптор файла, расположенный на более низком уровне драйвера устройства, чувствителен к размеру блока. Если функция возвращает 0, это означает, что ничего не записано; если она возвращает -1, в системном вызове write возникла ошибка, которая описывается в глобальной переменной errno,

Далее приведена синтаксическая запись.

#include <unistd.h>

size_t write(int fildes, const void *buf, size_t nbytes);

Благодаря полученным знаниям вы можете написать свою первую программу, simple_write.c:

#include <unistd.h>

#include <stdlib.h>

int main() {

 if ((write(1, "Here is some data\n", 18)) != 18)

  write(2, "A write error has occurred on file descriptor 1\n", 46);

 exit(0);

}

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

$ ./simple_write

Here is some data

$

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

read

Системный вызов read считывает до nbytes байтов данных из файла, ассоциированного с дескриптором файла fildes, и помещает их в область данных buf. Он возвращает количество действительно прочитанных байтов, которое может быть меньше требуемого количества. Если вызов read возвращает 0, ему нечего считывать; он достиг конца файла. Ошибка при вызове заставляет его вернуть -1.

#include <unistd.h>

size_t read(int fildes, void *buf, size_t nbytes);

Программа simple_read.c копирует первые 128 байтов стандартного ввода в стандартный вывод. Она копирует все вводимые данные, если их меньше 128 байтов.

#include <unistd.h>

#include <stdlib.h>

int main() {

 char buffer[128];

 int nread;

 nread = read(0, buffer, 128);

 if (nread == -1)

  write(2, "A read error has occurred\n", 26);

 if ((write(1, buffer, nread)) != nread)

  write(2, "A write error has occurred\n", 27);

 exit(0);

}

Если вы выполните программу, то получите следующий результат:

$ echo hello there | ./simple_read

hello there

$ ./simple_read < draft1.txt

Files

In this chapter we will be looking at files and directories and how to

manipulate them. We will learn how to create files, $

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

Примечание

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

open

Для создания дескриптора нового файла вы должны применить системный вызов open.

#include <fcntl.h>

#include <sys/types.h>

#include <sys/stat.h>

int open(const char *path, int oflags);

int open(const char *path, int oflags, mode_t mode);

Примечание

Строго говоря, для использования вызова open вы не должны включать файлы sys/types.h и sys/stat.h в системах, удовлетворяющих стандартам POSIX, но они могут понадобиться в некоторых системах UNIX.

Не вдаваясь в подробности, скажем, что вызов open устанавливает путь к файлу или устройству. Если установка прошла успешно, он возвращает дескриптор файла, который может применяться в системных вызовах read, write и др. Дескриптор файла уникален и не используется совместно другими процессами, которые могут в данный момент выполняться. Если файл открыт одновременно в двух программах, они поддерживают отдельные дескрипторы файла. Если они обе пишут в файл, то продолжат запись с того места, где остановились. Их данные не чередуются, но данные одной программы могут быть записаны поверх данных другой. У каждой программы свое представление о том, какая порция файла (каково смещение текущей позиции в файле) прочитана или записана. Вы можете помешать нежелательным накладкам такого сорта с помощью блокировки файла, которая будет обсуждаться в главе 7.

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

Параметр oflags задается как комбинация обязательного режима доступа к файлу и других необязательных режимов. Системный вызов open должен задавать один из режимов доступа к файлу, указанных в табл. 3.1.

Таблица 3.1

Режим Описание
О_RDONLY Открытие только для чтения
О_WRONLY Открытие только для записи
O_RDWR Открытие для чтения и записи

Вызов может также включать в параметр oflags комбинацию (с помощью побитовой операции OR) следующих необязательных режимов:

O_APPEND — помещает записываемые данные в конец файла;

O_TRUNC — задает нулевую длину файла, отбрасывая существующее содержимое;

O_CREAT — при необходимости создает файл с правами доступа, заданными в параметре mode;

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

Другие возможные значения параметра oflags описаны на странице интерактивного справочного руководства, посвященной open; ее можно найти в разделе 2 руководства (примените команду man 2 open).

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

Существует также системный вызов creat, стандартизованный POSIX, но он применяется не часто. Он не только создает файл, как можно ожидать; но также и открывает его. Такой вызов эквивалентен вызову open с параметром oflags, равным O_CREAT|О_WRONLY|O_TRUNC.

Количество файлов, одновременно открытых в любой выполняющейся программе, ограничено. Предельное значение обычно определяется константой OPEN_MAX в файле limits.h и меняется от системы к системе, но стандарт POSIX требует, чтобы оно было не меньше 16. Это значение само по себе может быть ограничено в соответствии с предельными значениями локальной системы, поскольку программа не сможет всегда иметь возможность держать открытыми такое количество файлов. В ОС Linux это предельное значение можно изменять во время выполнения и поэтому OPEN_MAX уже не константа. Как правило, ее начальное значение равно 256.

Исходные права доступа

Когда вы создаете файл, применяя флаг O_CREAT в системном вызове open, вы должны использовать форму с тремя параметрами. Третий параметр mode формируется из флагов, определенных в заголовочном файле sys/stat.h и соединенных поразрядной операцией OR. К ним относятся:

S_IRUSR — право на чтение, владелец;

S_IWUSR — право на запись, владелец;

S_IXUSR — право на выполнение, владелец;

S_IRGRP — право на чтение, группа;

S_IWGRP — право на запись, группа;

S_IXGRP — право на выполнение, группа;

S_IROTH — право на чтение, остальные;

S_IWOTH — право на запись, остальные;

S_IXOTH — право на выполнение, остальные.

Например, вызов

open("myfile", O_CREAT, S_IRUSR|S_IXOTH);

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

$ ls -ls myfile

0 -r-------х 1 neil software 0 Sep 22 08:11 myfile*

Есть пара факторов, способных повлиять на права доступа к файлу. Во-первых, заданные права применяются, только если файл создается. Во-вторых, на права доступа к созданному файлу оказывает воздействие маска пользователя (заданная командой командной оболочки, umask). Значение параметра mode, заданное в вызове open, на этапе выполнения объединяется с помощью операции AND с инвертированной маской пользователя. Например, если заданы маска пользователя 001 и в параметре mode флаг S_IXOTH, у созданного файла не будет права на выполнение для "остальных", т.к. маска пользователя указывает на то, что это право не должно предоставляться. Флаги в вызовах open и creat являются на самом деле запросами на установку прав доступа. Будут ли предоставлены запрошенные права, зависит от значения umask во время выполнения.

umask

umask — это системная переменная, содержащая маску для прав доступа к файлу, которые будут применяться при создании файла. Вы можете изменить значение переменной, выполнив команду umask, предоставляющую новое значение. Значение этой переменной представляет собой трёхзнаковое восьмеричное число. Каждая цифра — результат объединения с помощью операций OR значений 1, 2 или 4 (табл. 3.2). Отдельные цифры указывают на права доступа "пользователя", "группы" и "остальных" соответственно.

Таблица 3.2

Цифра Значение Смысл
1 0 Никакие права пользователя не отвергнуты
4 Право пользователя на чтение отвергается
2 Право пользователя на запись отвергается
1 Право пользователя на выполнение отвергается
2 0 Никакие права группы не отвергнуты
4 Право группы на чтение отвергается
2 Право группы на запись отвергается
1 Право группы на выполнение отвергается
3 0 Никакие права остальных не отвергнуты
4 Право остальных на чтение отвергается
2 Право остальных на запись отвергается
1 Право остальных на выполнение отвергается

Например, для блокирования права "группы" на запись и выполнение и права "остальных" на запись переменная umask должна была бы быть следующей (табл. 3.3).

Таблица 3.3

Цифра Значение
1 0
2 2
1
3 2

Значения каждой цифры объединяются операциями OR, поэтому для получения значения второй цифры нужна операция 2 | 1, дающая в результате 3. Результирующее значение umask — 032.

Когда вы создаете файл с помощью системного вызова open или creat, параметр mode сравнивается с текущим значением переменной umask. Любой бит, установленный в параметре mode и одновременно в переменной umask, удаляется. В результате пользователи могут настроить свое окружение, например, потребовав не создавать никаких файлов с правом на запись для остальных, даже если программа, создающая файл, требует предоставить такое право. Это не мешает программе или пользователю впоследствии применить команду chmod (или системный вызов chmod в программе), чтобы добавить право на запись для остальных, но поможет защитить пользователей, избавив их от необходимости проверять и задавать права доступа для всех новых файлов.

close

Системный вызов close применяется для разрыва связи файлового дескриптора fildes с его файлом. Дескриптор файла после этого может использоваться повторно. Вызов возвращает 0 в случае успешного завершения и -1 при возникновении ошибки.

#include <unistd.h>

int close (int fildes);

Примечание

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

ioctl

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

#include <unistd.h>

int ioctl(int fildes, int cmd, ...)

Вызов ioctl выполняет операцию, указанную в аргументе cmd, над объектом, заданным в дескрипторе fildes. У вызова может быть необязательный третий аргумент, зависящий от функций, поддерживаемых конкретным устройством.

Например, следующий вызов ioctl в ОС Linux включает световые индикаторы клавиатуры (LEDs).

ioctl(tty_fd, KDSETLED, LED_NUM|LED_CAP|LED_SCR);

Выполните упражнения 3.1 и 3.2.

Упражнение 3.1. Программа копирования файла

Теперь вы знаете достаточно о системных вызовах open, read и write, чтобы написать простенькую программу copy_system.c для посимвольного копирования одного файла в другой.

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

1. Сначала вам нужно создать тестовый входной файл размером, скажем, 1 Мбайт и именем file.in.

2. Далее откомпилируйте программу copy_system.c.

#include <unistd.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h>

int main() {

 char c;

 int in, out;

 in = open("file.in", O_RDONLY);

 put = open("file.out", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);

 while(read(in, &c, 1) == 1) write(out, &c, 1);

 exit(0);

}

Примечание

Имейте в виду, что строка #include <unistd.h> должна быть первой, поскольку она определяет флаги, касающиеся соответствия стандарту POSIX и способные повлиять на другие включенные в #include файлы.

3. Выполнение программы даст результат, похожий на следующий:

$ TIMEPORMAT="" time ./copy_system

4.67user 146.90system 2:32.57elapsed 99%CPU

...

$ ls -ls file.in file.out

1029 -rw-r--r-- 1 neil users 1048576 Sep 17 10:46 file.in

1029 -rw------- 1 neil users 1048576 Sep 17 10:51 file.out

Как это работает

Вы используете команду time для определения времени выполнения программы. В ОС Linux переменная TIMEFORMAT применяется для переопределения принятого по умолчанию в стандарте POSIX формата вывода времени, в который не включено время использования ЦПУ. Как видите, что в этой очень старой системе входной файл file.in размером 1 Мбайт был успешно скопирован в файл file.out, созданный с правами на чтение/запись только для владельца. Копирование заняло две с половиной минуты и затратило фактически все доступное время ЦПУ. Программа так медлительна потому, что вынуждена была выполнить более двух миллионов системных вызовов.

В последние годы ОС Linux продемонстрировала огромные успехи в повышении производительности системных вызовов и файловой системы. Для сравнения аналогичный тест с применением ядра 2.6 занял чуть менее 14 секунд:

$ TIMEFORMAT="" time ./copy_system

2.08user 10.59system 0:13.74elapsed 92%CPU

...

Упражнение 3.2. Вторая версия программы кодирования файла

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

#include <unistd.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h>

int main() {

 char block[1024];

 int in, out;

 int nread;

 in = open("file.in", O_RDONLY);

 out = open("file.out", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);

 while((nread = read(in, block, sizeof(block))) > 0)

  write(out, block, nread);

 exit(0);

}

Теперь испытайте программу, но сначала удалите старый выходной файл.

$ rm file.out

$ TIMEFORMAT="" time ./copy_block

0.00user 0.02system 0:00.04elapsed 78%CPU

...

Как это работает

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

Другие системные вызовы для управления файлами

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

lseek

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

#include <unistd.h>

#include <sys/types.h>

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

Параметр offset применяется для указания позиции, а параметр whence определяет способ применения offset и может принимать следующие значения:

SEEK_SEToffset задает абсолютную позицию;

SEEK_CURoffset задается относительно текущей позиции;

SEEK_ENDoffset задается относительно конца файла.

Вызов lseek возвращает величину параметра offset в байтах, измеряемую от начала файла, для которого установлен указатель, или -1 в случае неудачного завершения. Тип данных off_t, применяемый для параметра offset в операциях поиска, — зависящий от реализации тип integer (целое), определенный в файле sys/types.h.

fstat, stat и lstat

Системный вызов fstat возвращает информацию о состоянии файла, ассоциированного с открытым дескриптором файла. Эта информация записывается в структуру buf, адрес которой передается как параметр.

Далее приведена синтаксическая запись вызовов.

#include <unistd.h>

#include <sys/stat.h>

#include <sys/types.h>

int fstat(int fildes, struct stat *buf);

int stat(const char *path, struct stat *buf);

int lstat(const char *path, struct stat *buf);

Примечание

Учтите, что включение файла sys/types.h не обязательное, но мы рекомендуем включать его при использовании системных вызовов, поскольку некоторые из их определений применяют для стандартных типов псевдонимы, которые могут измениться когда-нибудь.

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

Элементы вызываемой структуры stat могут меняться в разных UNIX-подобных системах, но обязательно включают перечисленные в табл. 3.4 элементы.

Таблица 3.4

Элемент структуры stat  Описание
st_mode Права доступа к файлу и сведения о типе файла
st_ino Индекс, ассоциированный с файлом
st_dev Устройство, на котором размещен файл
st_uid Идентификатор (user identity) владельца файла
st_gid Идентификатор группы (group identity) владельца файла
st_atime Время последнего обращения
st_ctime Время последнего изменения прав доступа, владельца, группы или объема
st_mtime Время последней модификации содержимого
st_nlink Количество жестких ссылок на файл

У флагов st_mode, возвращаемых в структуре stat, также есть ряд ассоциированных макросов в заголовочном файле sys/stat.h. В эти макросы включены имена флагов для прав доступа и типов файлов и некоторые маски, помогающие проверять специфические типы и права.

Флаги прав доступа такие же, как в системном вызове open, описанном ранее. Для флагов типов файла включены следующие имена:

S_IFBLK — блочное устройство;

S_IFDIR — каталог;

S_IFCHR — символьное устройство;

S_IFIFO — FIFO (именованный канал);

S_IFREG — обычный файл;

S_IFLNK — символическая ссылка.

Для других флагов режима файла включены следующие имена:

S_ISUID — элемент получает setUID при выполнении;

S_ISGUID — элемент получает setGID при выполнении.

Для масок, интерпретирующих флаги st_mode, включены следующие имена:

S_IFMT — тип файла;

S_IRWXU — права пользователя на чтение/запись/выполнение;

S_IRWXG — права группы на чтение/запись/выполнение;

S_IRWXO — права остальных на чтение/запись/выполнение.

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

S_ISBLK — проверка для блочного файла;

S_ISCHR — проверка для символьного файла;

S_ISDIR — проверка для каталога;

S_ISFIFO — проверка для FIFO;

S_ISREG — проверка для обычного файла;

S_ISLNK — проверка для символической ссылки.

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

struct stat statbuf;

mode_t modes;

stat("filename", &statbuf);

modes = statbuf.st_mode;

if (!S_ISDIR(modes) && (modes & S_IRWXU) = S_IXUSR)

...

dup и dup2

Системные вызовы dup позволяют дублировать дескриптор файла, предоставляя два или несколько разных дескрипторов, обращающихся к одному и тому же файлу. Эта возможность может применяться для чтения и записи в разные части файла. Системный вызов dup дублирует файловый дескриптор fildes и возвращает новый дескриптор. Системный вызов dup2 умело копирует один дескриптор файла в другой, задавая дескриптор, применяемый для копии.

Далее приведена синтаксическая запись для вызовов.

#include <unistd.h>

int dup(int fildes);

int dup2(int fildes, int fildes2);

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

Стандартная библиотека ввода/вывода

Стандартная библиотека ввода/вывода (stdio) и ее заголовочный файл stdio.h предоставляют универсальный интерфейс для системных вызовов ввода/вывода нижнего уровня. Библиотека, теперь часть языка С стандарта ANSI, в отличие от системных вызовов, с которыми вы встречались ранее, включает много сложных функций для форматирования вывода и просмотра ввода. Она также обеспечивает необходимые условия буферизации для устройств.

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

Примечание

Не путайте эти потоки файлов с потоками ввода/вывода в языке С++ и механизмом STREAMS, описывающим взаимодействие процессов и введенным в системе AT&T UNIX System V Release 3, который не рассматривается в данной книге. Для получения дополнительной информации о средствах STREAMS обратитесь к спецификации X/Open (по адресу http://www.opengroup.org) и руководству по программированию AT&T STREAMS Programming Guide, поставляемому с системой System V.

Три файловых потока открываются автоматически при старте программы. К ним относятся stdin, stdout и stderr. Эти потоки объявлены в файле stdio.h и представляют вывод, ввод и стандартный поток ошибок, которым соответствуют низкоуровневые файловые дескрипторы 0, 1 и 2.

В данном разделе мы рассмотрим следующие функции:

fopen, fclose;

fread, fwrite;

fflush;

fseek;

fgetc, getc, getchar;

fputc, putc, putchar;

fgets, gets;

printf, fprintf и sprintf;

scanf, fscanf и sscanf;

fopen.

fopen

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

Далее приведена синтаксическая запись функции:

#include <stdio.h>

FILE *fopen(const char *filename, const char *mode);

Функция fopen открывает файл, заданный в параметре filename, и ассоциирует с ним поток. Параметр mode описывает, как файл должен быть открыт. Он задается одной из следующих строк:

□ "r" или "rb" — открыть только для чтения;

□ "w" или "wb" — открыть для записи, укоротить до нулевой длины;

□ "а" или "ab" — открыть для записи, дописывать в конец файла;

□ "r+" или "rb+" или "r+b" — открыть для изменения (чтение и запись);

□ "w+" или "wb+" или "w+b" — открыть для изменения, укоротить до нулевой длины;

□ "a+" или "ab+" или "а+b" — открыть для изменения, дописывать в конец файла. Символ b означает, что файл бинарный, а не текстовый.

Примечание

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

В случае успешного завершения функция fopen возвращает ненулевой указатель на структуру FILE*. В случае сбоя она вернет значение NULL, определенное в файле stdio.h.

Количество доступных потоков ограничено, как и число дескрипторов файлов. Реальное предельное значение содержится в определенной в файле stdio.h константе FOPEN_MAX и всегда не менее 8, а в ОС Linux обычно 16.

fread

Библиотечная функция fread применяется для чтения данных из файлового потока. Данные считываются из потока stream в буфер данных, заданный в параметре ptr. Функции fread и fwrite имеют дело с записями данных. Записи описываются размером size и количеством передаваемых записей nitems. Функция возвращает количество записей (а не байтов), успешно считанных в буфер данных. При достижении конца файла может быть возвращено меньше записей, чем nitems, вплоть до нуля.

Далее приведена синтаксическая запись функции:

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

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

fwrite

Интерфейс библиотечной функции fwrite аналогичен интерфейсу функции fread. Она принимает записи данных из заданного буфера данных и записывает их в поток вывода. Функция возвращает количество успешно записанных записей.

Далее приведена синтаксическая запись функции:

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);

Примечание

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

fclose

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

Далее приведена синтаксическая запись функции:

#include <stdio.h>

int fclose(FILE* stream);

fflush

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

Далее приведена синтаксическая запись функции:

#include <stdio.h>

int fflush(FILE *stream);

fseek

Функция fseek — это эквивалент для файлового потока системного вызова lseek. Она задает в stream позицию для следующей операции чтения этого потока или записи в него. Значения и смысл параметров offset и whence такие же, как у ранее описанных одноименных параметров вызова lseek. Но там, где lseek возвращает off_t, функция fseek возвращает целое число: 0, если выполнилась успешно, и -1 при аварийном завершении с ошибкой, указанной в переменной errno. Какое поле деятельности для стандартизации!

Далее приведена синтаксическая запись функции:

#include <stdio.h>

int fseek(FILE *stream, long int offset, int whence);

fgetc, getc и getchar

Функция fgetc возвращает из файлового потока следующий байт как символ. Когда она достигает конца файла или возникает ошибка, функция возвращает EOF. Для того чтобы различить эти два случая, следует применять функции ferror или feof.

Далее приведена синтаксическая запись функций:

#include <stdio.h>

int fgetc(FILE *stream);

int getc(FILE *stream);

int getchar();

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

Функция getchar эквивалентна вызову функции getc(stdin) и читает следующий символ из стандартного ввода.

fputc, putc и putchar

Функция fputc записывает символ в файловый поток вывода. Она возвращает записанное значение или EOF в случае аварийного завершения.

#include <stdio.h>

int fputc(int с, FILE *stream); int putc(int c, FILE *stream); int putchar(int c);

Как и в случае функций fgetc/getc, функция putc — эквивалент fputc, но может быть реализована как макрос.

Функция putchar — то же самое, что вызов putc(с, stdout), записывающий один символ в стандартный вывод. Имейте в виду, что функция putchar принимает, а функция getchar возвращает символы как данные типа int, а не char. Это позволяет индикатору конца файла (EOF) принимать значение -1, лежащее вне диапазона кодов символов.

fgets и gets

Функция fgets читает строку из файла ввода stream.

#include <stdio.h>

char *fgets(char *s, int n, FILE *stream);

char *gets(char *s);

Функция fgets пишет символы в строку, заданную указателем s, до тех пор, пока не встретится новая строка, либо не будет передано n-1 символов, либо не будет достигнут конец файла. Любая встретившаяся новая строка передается в строку, принимающую символы, и добавляется завершающий нулевой байт \0. Любой вызов передает максимум n-1 символов, т.к. должен быть вставлен нулевой байт, обозначающий конец строки и увеличивающий общее количество до n байтов.

При успешном завершении функция fgets возвращает указатель на строку s. Если поток указывает на конец файла, она устанавливает индикатор EOF для потока и возвращает пустой указатель. Если возникает ошибка чтения, fgets возвращает пустой указатель и устанавливает значение переменной errno, соответствующее типу ошибки.

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

Примечание

Учтите, что функция gets не ограничивает количество символов, которые могут передаваться, поэтому она может переполнить свой пересылочный буфер. По этой причине вам следует избегать применения этой функции и заменять ее функцией fgets. Многие проблемы безопасности порождены функциями в программах, сделанных для переполнения буфера тем или иным способом. Это одна из таких функций, поэтому будьте осторожны!

Форматированные ввод и вывод

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

printf, fprintf и sprintf

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

#include <stdio.h>

int printf(const char *format, ...);

int sprintf(char *s, const char *format, ...);

int fprintf(FILE * stream, const char *format, ...);

Функция printf выводит результат в стандартный вывод. Функция fprintf выводит результат в заданный файловый поток stream. Функция sprintf записывает результат и завершающий нулевой символ в строку s, передаваемую как параметр. Эта строка должна быть достаточно длинной, чтобы вместить весь вывод функции.

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

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

printf("Some numbers: %d, %d, and &d\n", 1, 2, 3);

Он порождает в стандартном выводе следующую строку.

Some numbers: 1, 2, and 3

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

Далее перечислены наиболее часто применяемые спецификаторы преобразований:

%d, %i — выводить целое как десятичное число;

, %x — выводить целое как восьмеричное, шестнадцатеричное число;

— выводить символ;

%s — выводить строку;

%f — выводить число с плавающей точкой (одинарной точности);

%e — выводить число с двойной точностью в формате фиксированной длины;

%g — выводить число двойной точности в общем формате.

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

Он может быть равен h, например, %hd для обозначения типа short int (короткие целые), или l, например, %ld для обозначения типа long int (длинные целые). Некоторые компиляторы могут проверять эти установки printf, но они ненадежны. Если вы применяете компилятор GNU gcc, можно вставить для этого в команду компиляции опцию -Wformat.

Далее приведен еще один пример:

char initial = 'А';

char *surname = "Matthew";

double age = 13.5;

printf("Hello Mr %c %s, aged %g\n", initial, surname, age);

Будет выводиться следующая информация:

Hello Mr A Matthew, aged 13.5

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

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

Таблица 3.5

Формат Аргумент Вывод
%10s "Hello" |     Hello|
%-10s "Hello" |Hello     |
%10d 1234 |      1234|
%-10d 1234 |1234      |
%010d 1234 |0000001234|
%10.4f 12.34 |   12.3400|
%*s 10, "Hello" |     Hello|

Все приведенные примеры выводятся в поле шириной 10 символов. Обратите внимание на то, что отрицательная ширина поля означает выравнивание элемента по левому краю в пределах поля. Переменная ширина поля обозначается символом "звездочка" (*). В этом случае следующий аргумент применяется для задания ширины. Ведущий ноль указывает на вывод элемента с ведущими нулями. В соответствии со стандартом POSIX функция printf не обрезает поля; наоборот она расширяет поле, чтобы вместить в него аргумент. Например, если вы попытаетесь вывести строку большей длины, чем заданное поле, ширина поля будет увеличена (табл. 3.6).

Таблица 3.6

Формат Аргумент Вывод
%10s "HelloTherePeeps" |HelloTherePeeps|

Функции семейства printf возвращают целое число, равное количеству выведенных символов. В случае функции sprintf в него не включается завершающий нуль-символ. При наличии ошибок эти функции возвращают отрицательное значение и задают переменную errno.

scanf, fscanf и sscanf

Семейство функций scanf действует аналогично функциям группы printf за исключением того, что эти функции читают элементы из потока и помещают их в переменные, адреса которых им передаются как параметры-указатели. Для управления преобразованиями ввода функции применяют строку format аналогичным образом и используют многие спецификаторы преобразований функций группы printf.

#include <stdio.h>

int scanf(const char *format, ...);

int fscanf(FILE *stream, const char *format, ...);

int sscanf(const char *s, const char *format, ...);

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

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

Рассмотрим простой пример:

int num;

scanf("Hello %d", &num);

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

Hello    1234

Hellol234

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

Примечание

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

К другим спецификаторам преобразований относятся следующие:

%d — считывание десятичного целого;

%o, %x — считывание восьмеричного, шестнадцатеричного целого;

%f, %e, %g — считывание числа с плавающей запятой;

%c — считывание символа (пробельный символ не пропускается);

%s — считывание строки;

%[] — считывание множества символов (см. последующее обсуждение);

%% — считывание знака %.

Как и в случае printf, у спецификаторов преобразований функции scanf есть ширина поля, ограничивающая объем ввода. Спецификатор размера (h для коротких или l для длинных целых) показывает, короче или длиннее стандартного получаемый аргумент. Таким образом, %hd обозначает число типа short int, %ld — число типа long int и %lg — число с плавающей точкой двойной точности.

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

Применяйте спецификатор %c для чтения одиночного символа во вводе. Он не пропускает начальные пробельные символы.

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

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

Применяйте спецификатор %[] для чтения строки, составленной из символов, включенных в множество. Формат %[A-Z] будет читать строку из прописных букв латинского алфавита. Если в множестве первый символ — знак вставки (^), то спецификатор считывает строку, состоящую из символов, не входящих в множество. Итак, для того чтобы прочитать строку с пробелами, но остановиться на первой запятой, примените спецификатор %[^, ].

Если задана следующая строка ввода:

Hello, 1234, 5.678, X, string to the end of the line

приведенный далее вызов scanf корректно считает четыре элемента:

char s[256];

int n;

float f;

char c;

scanf("Hello, %d, %g, %c, %[^\n]", &n, &f, &c, s);

Функции семейства scanf возвращают количество успешно считанных элементов. Оно может быть нулевым, если сбой возник при чтении первого элемента. Если достигнут конец ввода прежде, чем найдено соответствие первому элементу, возвращается EOF. Если в файловом потоке возникает ошибка чтения, устанавливается флаг ошибки потока и тип ошибки задается в переменной errno. Более подробную информацию см. в разд. "Ошибки потока" далее в этой главе.

Функция scanf и другие члены семейства, как правило, не высоко ценятся в основном по трем причинам:

□ традиционно их реализации полны ошибок;

□ в использовании эти функции не гибки;

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

В качестве альтернативы попытайтесь применять другие функции, такие как fread или fgets, для чтения строк ввода, а затем воспользуйтесь строковыми функциями для разделения введенной строки на нужные элементы.

Другие потоковые функции

В библиотеке stdio существует ряд других функций, использующих потоки как параметры или стандартные потоки stdin, stdout, stderr:

fgetpos — возвращает текущую позицию в файловом протоке;

fsetpos — устанавливает текущую позицию в файловом потоке;

ftell — возвращает величину текущего смещения файла в потоке;

rewind — сбрасывает текущую позицию файла в потоке и переводит ее в начало файла;

freopen — повторно использует файловый поток;

setvbuf — задает схему буферизации для потока;

remove — эквивалент функции unlink, до тех пор пока параметр path не является каталогом, в этом случае она эквивалентна функции rmdir.

Эти библиотечные функции описаны на страницах интерактивного справочного руководства в разделе 3.

Вы можете использовать функции обработки файловых потоков для повторной реализации с их помощью программы копирования файлов. Взгляните на программу copy_stdio.c в упражнении 3.3.

Упражнение 3.3. Третья версия программы копирования файлов

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

#include <stdio.h>

#include <stdlib.h>

int main() {

 int c; 

 FILE *in, *out;

 in = fopen("file.in", "r");

 out = fopen("file.out", "w");

 while((c = fgetc(in)) != EOF) fputc(c, out);

 exit(0);

}

Выполнив эту программу, как прежде, вы получите:

$ TIMEFORMAT="" time ./copy_stdio

0.06user 0.02system 0:00.11elapsed 81%CPU

Как это работает

На этот раз программа выполняется 0,11 с, не так быстро, как низкоуровневая блочная версия, но значительно быстрее другой посимвольной версии. Это произошло потому, что библиотека stdio поддерживает внутренний буфер в структуре FILE, и низкоуровневые системные вызовы выполняются, только когда буфер заполняется. Не ленитесь экспериментировать, тестируя программный код построчного и блочного копирования с помощью stdio, чтобы увидеть, как они действуют в случае проверенных нами трех примеров.

Ошибки потока

Для обозначения ошибок многие функции библиотеки stdio применяют значения за пределами допустимых, например, пустые указатели или константу EOF. В этих случаях ошибка указывается во внешней переменной errno.

#include <errno.h>

extern int errno;

Примечание

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

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

#include <stdio.h>

int ferror(FILE *stream);

int feof(FILE *stream);

void clearerr(FILE *stream);

Функция ferror проверяет индикатор ошибок потока и возвращает ненулевое значение, если индикатор установлен, и ноль в противном случае.

Функция feof проверяет индикатор конца файла в потоке и возвращает ненулевое значение, если индикатор установлен, или ноль в противном случае. Применяйте ее следующим образом:

if (feof(some_stream))

 /* Мы в конце */

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

Потоки и дескрипторы файлов

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

#include <stdio.h>

int fileno(FILE *stream);

FILE *fdopen(int fildes, const char *mode);

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

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

Функция fdopen действует так же, как функция fopen, но в отличие от имени файла она принимает в качестве параметра низкоуровневый дескриптор файла. Это может пригодиться, если вы используете вызов open для создания файла, может быть для более тонкого управления правами доступа, но хотите применить поток для записи в файл. Параметр mode такой же, как у функции fopen и должен быть совместим с режимами доступа к файлу, установленными при первоначальном открытии файла. Функция fdopen возвращает новый файловый поток или NULL в случае неудачного завершения.

Ведение файлов и каталогов

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

chmod

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

Далее приведена синтаксическая запись вызова:

#include <sys/stat.h>

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

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

chown

Суперпользователь может изменить владельца файла с помощью системного вызова chown.

#include <sys/types.h> #include <unistd.h>

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

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

Примечание

Стандарт POSIX в действительности допускает существование систем, в которых несуперпользователи могут изменять права владения файлом. Все "правильные" с точки зрения POSIX системы не допускают этого, но строго говоря, это расширение стандарта (в FIPS 151-2). Все виды систем, с которыми мы имеем дело в этой книге, подчиняются спецификации XSI (X/Open System Interface) и соблюдают на деле правила владения.

unlink, link и symlink

С помощью вызова unlink вы можете удалить файл.

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

#include <unistd.h>

int unlink(const char *path);

int link(const char *path1, const char *path2);

int symlink(const char *path1, const char *path2);

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

Примечание

Создание файла с помощью вызова open и последующее обращение к unlink для этого файла — трюк, применяемый некоторыми программистами для создания временных или транзитных файлов. Эти файлы доступны программе, только пока они открыты; и будут удалены автоматически, когда программа завершится, и файлы будут закрыты.

Системный вызов link создает новую ссылку на существующий файл path1. Новый элемент каталога задается в path2. Символические ссылки можно создавать аналогичным образом с помощью системного вызова symlink. Имейте в виду, что символические ссылки на файл не увеличивают значение счетчика ссылок и таким образом, в отличие от обычных (жестких) ссылок, не мешают удалению файла.

mkdir и rmdir

Вы можете создавать и удалять каталоги, применяя системные вызовы mkdir и rmdir.

#include <sys/types.h>#include <sys/stat.h>

int mkdir(const char *path, mode_t mode);

Системный вызов mkdir используется для создания каталогов и эквивалентен программе mkdir. Вызов mkdir формирует новый каталог с именем, указанным в параметре path. Права доступа к каталогу передаются в параметре mode и задаются как опция о O_CREAT в системном вызове open и также зависят от переменной umask.

#include <unistd.h>

int rmdir(const char *path);

Системный вызов rmdir удаляет каталоги, но только если они пустые. Программа rmdir использует этот системный вызов для выполнения аналогичной работы.

chdir и getcwd

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

#include <unistd.h>

int chdir(const char *path);

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

#include <unistd.h>

char *getcwd(char *buf, size_t size);

Функция getcwd записывает имя текущего каталога в заданный буфер buf. Она возвращает NULL, если имя каталога превысит размер буфера (ошибка ERANGE), заданный в параметре size. В случае успешного завершения она возвращает buf.

Функция getcwd может также вернуть значение NULL, если во время выполнения программы каталог удален (EINVAL) или изменились его права доступа (EACCESS).

Просмотр каталогов

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

Функции работы с каталогами объявлены в заголовочном файле dirent.h. В них используется структура DIR как основа обработки каталогов. Указатель на эту структуру, называемый потоком каталога (DIR*), действует во многом так же, как действует поток файла (FILE*) при работе с обычным файлом. Элементы каталога возвращаются в структурах dirent, также объявленных в файле dirent.h, поскольку никому не следует изменять поля непосредственно в структуре DIR.

Мы рассмотрим следующие функции:

opendir, closedir;

readdir;

telldir;

seekdir;

closedir.

opendir

Функция opendir открывает каталог и формирует поток каталога. Если она завершается успешно, то возвращает указатель на структуру DIR, которая будет использоваться для чтения элементов каталога.

#include <sys/types.h>

#include <dirent.h>

DIR *opendir(const char *name);

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

readdir

Функция readdir возвращает указатель на структуру, содержащую следующий элемент каталога в потоке каталога dirp. Успешные вызовы readdir возвращают следующие элементы каталогов. При возникновении ошибки и в конце каталога readdir возвращает NULL. Системы, удовлетворяющие стандарту POSIX, возвращая NULL, не меняют переменную errno в случае достижения конца каталога и устанавливают ее значение, если обнаружена ошибка.

#include <sys/types.h>

#include <dirent.h>

struct dirent *readdir(DIR *dirp);

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

В состав структуры dirent, содержащей реквизиты элемента каталога, входят следующие компоненты.

ino_t d_ino — индекс файла;

char d_name[] — имя файла.

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

telldir

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

#include <sys/types.h>

#include <dirent.h>

long int telldir(DIR *dirp);

seekdir

Функция seekdir устанавливает указатель на элемент каталога в потоке каталога, заданном в параметре dirp. Значение параметра loc, применяемого для установки позиции, следует получить из предшествующего вызова функции telldir.

#include <sys/types.h>

#include <dirent.h>

void seekdir (DIR *dirp, long int loc);

closedir

Функция closedir закрывает поток каталога и освобождает ресурсы, выделенные ему. Она возвращает 0 в случае успеха и -1 при наличии ошибки.

#include <sys/types.h>

#include <dirent.h>

int closedir(DIR *dirp);

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

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

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

Для того чтобы познакомиться с методами повышения универсальности программ, посмотрите исходный код таких утилит Linux, как ls и find.

Упражнение 3.4. Программа просмотра каталога

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

#include <unistd.h>

#include <stdio.h>

#include <dirent.h>

#include <string.h>

#include <sys/stat.h>

#include <stdlib.h>

void printdir(char *dir, int depth) {

 DIR *dp;

 struct dirent *entry;

 struct stat statbuf;

 if ((dp = opendir(dir)) == NULL) {

  fprintf(stderr, "cannot open directory: %s\n", dir);

  return;

 }

 chdir(dir);

 while((entry = readdir(dp)) != NULL) {

  lstat(entry->d_name, &statbuf);

  if (S_ISDIR(statbuf.st_mode)) {

   /* Находит каталог, но игнорирует . и .. */

   if (strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0)

    continue;

   printf("%*s%s/\n", depth, "", entry->d_name);

   /* Рекурсивный вызов с новый отступом */

   printdir(entry->d_name, depth+4);

  } else printf("%*s%s\n", depth, " ", entry->d_name);

 }

 chdir("..");

 closedir(dp);

}

2. Теперь переходите к функции main.

int main() {

 /* Обзор каталога /home */

 printf("Directory scan of /home:\n");

 printdir("/home", 0);

 printf("done.\n");

 exit(0);

}

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

$ ./printdir

Directory scan of /home:

neil/

    .Xdefaults

    .Xmodmap

    .Xresources

    .bash_history

    .bashrc

    .kde/

        share/

            apps/

                konqueror/

                    dirtree/

                        public_html.desktop

                    toolbar/

                        bookmarks.xml

                        konq_history

                    kdisplay/

                        color-schemes/

    BLP4e/

        Gnu_Public_License

        chapter04/

            argopt.с

            args.с

        chapter03/

            file.out

            mmap.с

            printdir

done.

Как это работает

Большинство операций сосредоточено в функции printdir. После некоторой начальной проверки ошибок с помощью функции opendir, проверяющей наличие каталога, printdir выполняет вызов функции chdir для заданного каталога. До тех пор пока элементы, возвращаемые функцией readdir, не нулевые, программа проверяет, не является ли очередной элемент каталогом. Если нет, она печатает элемент-файл с отступом, равным depth.

Если элемент — каталог, вы встречаетесь с рекурсией. После игнорирования элементов . и .. (текущего и родительского каталогов) функция printdir вызывает саму себя и повторяет весь процесс снова. Как она выбирается из этих повторений? Как только цикл while заканчивается, вызов chdir("..") возвращает программу вверх по дереву каталогов, и предыдущий перечень можно продолжать. Вызов closedir(dp) гарантирует, что количество открытых потоков каталогов не больше того, которое должно быть.

Для того чтобы составить представление об окружении в системе Linux, обсуждаемом в главе 4, познакомьтесь с одним из способов, повышающих универсальность программы. Рассматриваемая программа ограничена, потому что привязана каталогу /home. Следующие изменения в функции main могли бы превратить эту программу в полезный обозреватель каталогов:

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

 char *topdir = ".";

 if (argc >= 2) topdir = argv[1];

 printf("Directory scan of %s\n", topdir);

 printdir(topdir, 0);

 printf("done.\n");

 exit(0);

}

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

$ ./printdir2 /usr/local | more

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

Ошибки 

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

Имена констант и варианты ошибок перечислены в заголовочном файле errno.h. К ним относятся следующие:

EPERM — Operation not permitted (операция не разрешена);

ENOENT — No such file or directory (нет такого файла или каталога);

EINTR — Interrupted system call (прерванный системный вызов);

EIO — I/O Error (ошибка ввода/вывода);

EBUSY — Device or resource busy (устройство или ресурс заняты);

EEXIST — File exists (файл существует);

EINVAL — Invalid argument (неверный аргумент);

EMFILE — Too many open files (слишком много открытых файлов);

ENODEV — No such device (нет такого устройства);

EISDIR — Is a directory (это каталог);

ENOTDIR — Isn't a directory (это не каталог).

Есть пара полезных функций, сообщающих об ошибках при их возникновении: strerror и perror.

strerror

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

Далее приведена ее синтаксическая запись:

#include <string.h>

char *strerror(int errnum);

perror

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

Далее приведена синтаксическая запись функции:

#include <stdio.h>

void perror(const char *s);

Например, вызов

perror("program");

может дать следующий результат в стандартном потоке ошибок:

program: Too many open files

Файловая система procfs

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

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

В прошлом для связи с драйверами устройств применялись утилиты общего назначения. Например, hdparm использовалась для настройки некоторых параметров диска, a ifconfig могла сообщить сетевую статистику. В недавнем прошлом появилась тенденция, направленная на обеспечение более подходящего способа доступа к информации драйвера и, как расширение, включающая взаимодействие с различными элементами ядра Linux.

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

Набор файлов в каталоге /proc меняется от системы к системе, и с каждым новым выпуском Linux появляются новые файлы, дополнительные драйверы и средства поддержки файловой системы procfs. В этом разделе мы рассмотрим некоторые из самых широко распространенных файлов и кратко обсудим их применение.

В перечень каталога /proc на компьютере, использовавшемся для написания этой главы, включены следующие элементы:

1/     10514/ 20254/ 6/    9057/ 9623/     ide/       mtrr

10359/ 10524/ 29/    698/  9089/ 9638/     interrupts net/

10360/ 10530/ 983/   699/  9118/ acpi/     iomem      partitions

10381/ 10539/ 3/     710/  9119/ asound/   ioports    scsi/

10438/ 10541/ 30/    711/  9120/ buddyinfo irq/       self@

10441/ 10555/ 3069/  742/  9138/ bus/      kallsyms   slabinfo

10442/ 10688/ 3098/  7808/ 9151/ cmdline   kcore      splash

10478/ 10689/ 3099/  7813/ 92/   config.gz keys       stat

10479/ 10784/ 31/    8357/ 9288/ cpuinfo   key-users  swaps

10482/ 113/   3170/  8371/ 93/   crypto    kmsg       sys/

10484/ 115/   3171/  840/  9355/ devices   loadavg    sysrq-trigger

10486/ 116/   3177/  8505/ 9407/ diskstats locks      sysvipc/

10495/ 1167/  32288/ 8543/ 9457/ dma       mdstat     tty/

10497/ 1168/  3241/  8547/ 9479/ driver/   meminfo    uptime

Во многих случаях файлы могут только читаться и дают информацию о состоянии. Например, /proc/cpuinfo предоставляет сведения о доступных процессорах:

$ cat /proc/cpuinfo

processor    : 0

vendor_id     : GenuineIntel

cpu family    : 15

model         : 2

model name    : Intel(R) Pentium(R) 4 CPU 2.66GHz

stepping      : 8

cpu MHz       : 2665.923

cache size    : 512 KB

fdiv_bug      : no

hlt_bug       : no

f00f_bug      : no

coma_bug      : no

fpu           : yes

fpu_exception : yes

cpuid level   : 2

wp            : yes

flags         : fpu vme de pse tsc msr рае mce cx8 apic sep mtrr pge mca cmov

pat pse36 clflush dts acpi mmx fxsr sse sse2 ss up

bogomips      : 5413.47

clflush size  : 64

Файлы /proc/meminfo и /рroc/version предоставляют данные об использовании оперативной памяти и версии ядра соответственно:

$ cat /proc/meminfo

MemTotal:     776156 kB

MemFree:       28528 kB

Buffers:      191764 kB

Cached:       369520 kB

SwapCached:       20 kB

Active:       406912 kB

Inactive:     274320 kB

HighTotal:         0 kB

HighFree:          0 kB

LowTotal:     776156 kB

LowFree:       28528 kB

SwapTotal:   1164672 kB

SwapFree:    1164652 kB

Dirty:            68 kB

Writeback:         0 kB

AnonPages:     95348 kB

Mapped:        49044 kB

Slab:          57848 kB

SReclaimable:  48008 kB

SUnreclaim:     9840 kB

PageTables:     1500 kB

NFS_Unstable:      0 kB

Bounce:            0 kB

CommitLimit: 1552748 kB

Committed_AS: 189680 kB

VmallocTotal: 245752 kB

VmallocUsed:   10572 kB

VmallocChunk: 234556 kB

HugePages_Total:   0

HugePages_Free:    0

HugePages_Rsvd:    0

Hugepagesize:   4096 kB

$ cat /proc/version

Linux version 2.6.20.2-2-default (geeko@buildhost) (gcc version 4.1.3 20070218 (prerelease) (SUSE Linux)) #1 SMP Fri Mar 9 21:54:10 UTC 2007

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

Получить дополнительную информацию от специальных функций ядра можно в подкаталогах каталога /proc. Например, статистику использования сетевых сокетов вы можете узнать из /proc/net/sockstat:

$ cat /proc/net/sockstat

sockets: used 285

TCP: inuse 4 orphan 0 tw 0 alloc 7 mem 1

UDP: inuse 3

UDPLITE: inuse 0

RAW: inuse 0

FRAG: inuse 0 memory 0

В некоторые элементы каталога /proc можно производить запись, а не только читать их. Например, общее количество файлов, которые могут быть открыты одновременно всеми выполняющимися программами, — это параметр ядра Linux. Текущее значение можно прочитать из /proc/sys/fs/file-max:

$ cat /proc/sys/fs/file-max

76593

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

Примечание

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

Для увеличения предельного значения одновременно обрабатываемых в системе файлов до 80000 вы можете просто записать новое предельное значение в файл file-max.

# echo 80000 >/proc/sys/fs/file-max

Теперь, повторно прочитав файл, вы увидите новое значение:

$ cat /proc/sys/fs/file-max

80000

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

Сейчас только отметьте, что у каждого процесса есть уникальный идентификатор: число в диапазоне от 1 до почти 32 000. Команда ps предоставляет список выполняющихся в данный момент процессов. Например, когда писалась эта глава:

neil@susel03:~/BLP4e/chapter03> ps -а

  PID TTY       TIME CMD

 9118 pts/1 00:00:00 ftp

 9230 pts/1 00:00:00 ps

10689 pts/1 00:00:01

bash neil@susel03:~/BLP4e/chapter03>

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

В данном случае для ftp задан идентификатор процесса 9118, поэтому вы должны заглянуть в каталог /proc/9118 для получения подробной информации о нем:

$ ls -l /proc/9118

total 0

0 dr-xr-xr-x 2 neil users 0 2007-05-20 07:43 attr

0 -r-------- 1 neil users 0 2007-05-20 07:43 auxv

0 -r--r--r-- 1 neil users 0 2007-05-20 07:35 cmdline

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 cpuset

0 lrvxrwxrwx 1 neil users 0 2007-05-20 07:43 cwd -> /home/neil/BLP4e/chapter03

0 -r-------- 1 neil users 0 2007-05-20 07:43 environ

0 lrwxrwxrwx 1 neil users 0 2007-05-20 07:43 exe -> /usr/bin/pftp

0 dr-x------ 2 neil users 0 2007-05-20 07:19 fd

0 -rw-r--r-- 1 neil users 0 2007-05-20 07:43 loginuid

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 maps

0 -rw------- 1 neil users 0 2007-05-20 07:43 mem

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 mounts

0 -r-------- 1 neil users 0 2007-05-20 07:43 mountstats

0 -rw-r--r-- 1 neil users 0 2007-05-20 07:43 oom_adj

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 oom_score

0 lrwxrwxrwx 1 neil users 0 2007-05-20 07:43 root -> /

0 -rw------- 1 neil users 0 2007-05-20 07:43 seccomp

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 smaps

0 -r--r--r-- 1 neil users 0 2007-05-20 07:33 stat

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 statm

0 -r--r--r-- 1 neil users 0 2007-05-20 07:33 status

0 dr-xr-xr-x 3 neil users 0 2007-05-20 07:43 task

0 -r--r--r-- 1 neil users 0 2007-05-20 07:43 wchan

В данном перечне вы видите разные специальные файлы, способные сообщить вам, что происходит с процессом.

Можно сказать, что выполняется программа /usr/bin/pftp, и ее текущий рабочий каталог — home/neil/BLP4e/chapter03. Есть возможность прочитать другие файлы из этого каталога, чтобы увидеть командную строку, применяемую для запуска программы, а также ее окружение. Файлы cmdline и environ предоставляют эту информацию в виде последовательности нуль-терминированных строк, поэтому вам следует соблюдать осторожность при их просмотре. Более подробно окружение ОС Linux мы обсудим в главе 4.

$ od -с /proc/9118/cmdline

0000000 f  t  p \0  1  9  2  .  1  6  8  .  0  .  1  2

0000020 \0

0000021

Из полученного вывода видно, что ftp была запущена из командной строки ftp 192.163.0.12.

Подкаталог fd предоставляет информацию об открытых дескрипторах файлов, используемых процессом. Эти данные могут быть полезны при определении количества файлов, одновременно открытых программой. На каждый открытый дескриптор приходится один элемент; имя его соответствует номеру дескриптора. В нашем случае, как мы и ожидали, у программы ftp есть открытые дескрипторы 0, 1, 2 и 3. Они включают стандартные дескрипторы ввода, вывода и потока ошибок плюс подключение к удаленному серверу.

$ ls /proc/9118/fd

0 1 2 3

Более сложные приемы: fcntl и mmap

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

fcntl

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

#include <fcntl.h>

int fcntl(int fildes, int cmd);

int fcntl(int fildes, int cmd, long arg);

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

Различные операции выбираются разными значениями параметра команды cmd, как определено в файле fcntl.h. В зависимости от выбранной команды системному вызову может потребоваться третий параметр arg.

fcntl(fildes, F_DUPFD, newfd) — этот вызов возвращает новый дескриптор файла с числовым значением, равным или большим целочисленного параметра newfd. Новый дескриптор — копия дескриптора fildes. В зависимости от числа открытых файлов и значения newfd этот вызов может быть практически таким же, как вызов dup(fildes).

fcntl(fildes, F_GETFD) — этот вызов возвращает флаги дескриптора файла, как определено в файле fcntl.h. К ним относится FD_CLOEXEC, определяющий, закрыт ли дескриптор файла после успешного вызова одного из системных вызовов семейства exec.

fcntl(fildes, F_SETFD, flags) — этот вызов применяется для установки флагов дескриптора файла, как правило, только FD_CLOEXEC.

fcntl(fildes, F_GETFL) и fcntl(fildes, F_SETFL, flags) — эти вызовы применяются, соответственно, для получения и установки флагов состояния файла и режимов доступа. Вы можете извлечь режимы доступа к файлу с помощью маски O_ACCMODE, определенной в файле fcntl.h. Остальные флаги включают передаваемые значения в третьем аргументе вызову open с использованием O_CREAT. Учтите, что вы не можете задать все флаги. В частности, нельзя задать права доступа к файлу с помощью вызова fcntl.

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

mmap

Система UNIX предоставляет полезное средство, позволяющее программам совместно использовать память, и, к счастью, оно включено в версию 2.0 и более поздние версии ядра Linux. Функция mmap (для отображения памяти) задает сегмент памяти, который может читаться двумя или несколькими программами и в который они могут записывать данные. Изменения, сделанные одной программой, видны всем остальным.

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

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

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

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);

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

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

Параметр prot используется для установки прав доступа к сегменту памяти. Он представляет собой результат поразрядной операции or, примененной к следующим константам:

PROT_READ — сегмент может читаться;

PROT_WRITE — в сегмент можно писать;

PROT_EXEC — сегмент может выполняться;

PROT_NONE — к сегменту нет доступа.

Параметр flags контролирует, как изменения, сделанные программой в сегменте, отражаются в других местах; его возможные значения приведены в табл. 3.7.

Таблица 3.7

Константа Описание
MAP_PRIVATE Сегмент частный, изменения локальные
MAP_SHARED Изменения сегмента переносятся в файл
MAP_FIXED Сегмент должен располагаться по заданному адресу addr

Функция msync вызывает запись изменений в части или во всем сегменте памяти обратно а отображенный файл (или считывание из файла).

#include <sys/mman.h>

int msync(void *addr, size_t len, int flags);

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

Таблица 3.8

Константа Описание
MS_ASYNC Выполнять запись асинхронно
MS_SYNC Выполнять запись синхронно
MS_INVALIDATE Обновить другие отражения этого файла так, чтобы они содержали изменения, внесенные этим вызовом

Функция munmap освобождает сегмент памяти.

#include <sys/mman.h>

int munmap(void *addr, size_t len);

В программе mmap.с из упражнения 3.5 показан файл из структур, которые будут корректироваться с помощью функции mmap и обращений в стиле массива. Ядро Linux версий, меньших 2.0, не полностью поддерживает применение функции mmap. Программа работает корректно в системе Sun Solaris и других системах.

Упражнение 3.5. Применение функции mmap

1. Начните с определения структуры RECORD и создайте NRECORDS вариантов, в каждый из которых записывается собственный номер. Они будут добавлены в конец файла records.dat.

#include <unistd.h>

#include <stdio.h>

#include <sys/mman.h>

#include <fcntl.h>

#include <stdlib.h>

typedef struct {

 int integer;

 char string[24];

} RECORD;

#define NRECORDS (100)

int main() {

 RECORD record, *mapped;

 int i, f;

 FILE *fp;

 fp = fopen("records.dat", "w+");

 for (i=0; i<NRECORDS; i++) {

  record.integer = i;

  sprintf(record.string, "RECORD-%d", i);

  fwrite(&record, sizeof(record), 1, fp);

 }

 fclose(fp);

2. Далее измените целое значение записи с 43 на 143 и запишите его в строку 43-й записи.

 fp = fopen("records.dat", "r+");

 fseek(fp, 43*sizeof(record), SEEK_SET);

 fread(&record, sizeof(record), 1, fp);

 record.integer =143;

 sprintf(record.string, "RECORD-%d", record.integer);

 fseek(fp, 43*sizeof(record), SEEK_SET);

 fwrite(&record, sizeof(record), 1, fp);

 fclose(fp);

3. Теперь отобразите записи в память и обратитесь к 43-й записи для того, чтобы изменить целое на 243 (и обновить строку записи), снова используя отображение в память.

 f = open("records.dat", O_RDWR);

 mapped = (RECORD *)mmap(0, NRECORDS*sizeof(record),

  PROT_READ|PROT_WRITE, MAP_SHARED, f, 0);

 mapped[43].integer = 243;

 sprintf(mapped[43].string, "RECORD-%d", mapped[43].integer);

 msync((void *)mapped, NRECORDS*sizeof(record), MS_ASYNC);

 munmap((void *)mapped, NRECORDS*sizeof(record));

 close(f);

 exit(0);

}

В главе 13 вы встретитесь с еще одним средством совместного использования памяти — разделяемой памятью System V.

Резюме

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

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

Глава 4

Окружение Linux

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

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

□ передача аргументов в программы;

□ переменные окружения;

□ определение текущего времени;

□ временные файлы;

□ получение информации о пользователе и рабочем компьютере;

□ формирование и настройка регистрируемых сообщений;

□ выявление ограничений, накладываемых системой.

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

Когда в ОС Linux или UNIX выполняется программа на языке С, она начинается с функции main. В таких программах функция main объявляется следующим образом:

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

Здесь argc — это счетчик аргументов программы, a argv — массив символьных строк, представляющих сами аргументы.

Вы можете встретить программы на языке С для ОС Linux, просто объявляющие функцию main как

main()

Этот вариант тоже работает, поскольку по умолчанию возвращаемому функцией значению будет назначен тип int, а формальные параметры, которые в функции не применяются, не нуждаются в объявлении. Параметры argc и argv остаются на своем месте, но если вы не объявляете их, то и не можете их использовать.

Каждый раз, когда операционная система запускает новую программу, параметры argc и argv устанавливаются и передаются функции main. Обычно эти параметры предоставляются другой программой, часто командной оболочкой, которая запросила у операционной системы запуск новой программы. Оболочка принимает заданную командную строку, разбивает её на отдельные слова и использует их для заполнения массива argv. Помните о том, что до установки параметров argc и argv командная оболочка Linux обычно выполняет раскрытие метасимволов в аргументах, содержащих имена файлов, в то время как оболочка MS-DOS рассчитывает на то, что программы примут аргументы с метасимволами и выполнят собственную постановку.

Например, если мы дадим командной оболочке следующую команду:

$ myprog left right 'and center'

программа myprog запустит функцию main с приведенными далее параметрами.

argc: 4

argv: {"myprog", "left", "right", "and center"}

Обратите внимание на то, что аргумент-счётчик содержит имя программы и в массив argv оно включено как первый элемент argv[0]. Поскольку в команде оболочки мы применили кавычки, четвертый аргумент представляет собой строку, содержащую пробелы.

Вам все это знакомо, если вы программировали на языке С стандарта ISO/ANSI, Аргументы функции main соответствуют позиционным параметрам в сценариях командной оболочки: $0, $1 и т.д. Язык ISO/ANSI С заявляет, что функция main должна возвращать значение типа int, спецификация X/Open содержит явное объявление, данное ранее.

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

$ sort -r файл

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

$ tar cvfB /tmp/file.tar 1024

dd if=/dev/fd0 of=/trap/file.dd bs=18k

$ ps ax

$ gcc --help

$ ls -lstr

$ ls -l -s -t -r

Мы рекомендуем в ваших приложениях все переключатели командной строки начинать с дефиса и делать их односимвольными, состоящими из одной буквы или цифры. При необходимости опции, не содержащие последующих аргументов, могут группироваться вместе после общего дефиса. Таким образом, два только что приведенных примера с командой ls соответствуют нашим рекомендациям. За каждой опцией может следовать любое необходимое ей значение как отдельный аргумент. Пример с программой dd нарушает наше правило, поскольку использует многосимвольные опции, которые начинаются совсем не с дефисов (if=/dev/fd0): в примере с программой tar опции полностью оторваны от своих значений! Целесообразно добавлять более длинные и информативные имена переключателей как альтернативу односимвольных вариантов и использовать двойной дефис для их выделения. Таким образом, у нас могут быть два варианта опции получения помощи: -h и --help.

Еще один недостаток некоторых программ — создание опции +x (например) для выполнения функции, противоположной . В главе 2 мы применяли команду set -о xtrace для включения отслеживания действий командной оболочки и команду set +о xtrace для выключения этого режима.

Вы, вероятно, можете сказать, что запомнить порядок и назначение всех этих программных опций достаточно трудно без необходимости освоения вызывающих идиосинкразию форматов. Часто единственный выход — применение опции -h (от англ. help) или страниц интерактивного справочного руководства (man), если программист предоставил одну из этих возможностей. Чуть позже в этой главе мы покажем, что функция getopt предоставляет изящное решение этих проблем. А сейчас, тем не менее, в упражнении 4.1 давайте посмотрим, как передаются аргументы программы.

Упражнение 4.1. Аргументы программы

Далее приведена программа args.c, проверяющая собственные аргументы.

#include <stdio.h>

#include <stdlib.h>

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

 int arg;

 for (arg = 0; arg < argc; arg++) {

  if (argv[arg][0] == '-')

printf("option: %s\n", argv[arg]+1);

  else

   printf("argument %d: %s\n", arg, argv[arg]);

 }

 exit(0);

}

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

$ ./args -i -lr 'hi there' -f fred.c

argument 0: ./args

option: i

option: lr

argument 3: hi there option: f

argument 5: fred.с

Как это работает

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

В данном примере, если мы предполагаем, что доступны опции -l и -r, то упускаем тот факт, что группа -lr, возможно, должна интерпретироваться так же, как -l и -r.

В стандарте X/Open (который можно найти по адресу http://opengroup.org/) определено стандартное применение опций командной строки (Utility Syntax Guidelines, руководство по синтаксису утилит) и стандартный программный интерфейс для представления переключателей командной строки в программах на языке С: функция getopt.

getopt

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

#include <unistd.h>

int getopt(int argc, char *const argv[], const char *optstring);

extern char *optarg;

extern int optind, opterr, optopt;

Функция getopt принимает параметры argc и argv в том виде, в каком они передаются функции main в программе, и строку спецификатора опций, которая сообщает getopt, какие опции определены для программы и есть ли у них связанные с ними значения. optstring — это просто список символов, каждый из которых представляет односимвольную опцию. Если за символом следует двоеточие, это означает, что у опции есть ассоциированное значение, которое будет принято как следующий аргумент. Команда getopt оболочки bash выполняет аналогичную функцию.

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

getopt(argc, argv, "if:lr");

В нем учтены простые опции -i, -l, -r и -f, за которыми последует аргумент с именем файла. Вызов команды с теми же параметрами, но указанными в другом порядке, изменит поведение. Вы сможете попробовать сделать это, когда получите пример кода из упражнения 4.2.

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

□ Если опция принимает значение, на него указывает внешняя переменная optarg.

□ Функция getopt вернет -1, когда не останется опций для обработки. Специальный аргумент -- заставит getopt прекратить перебор опций.

□ Функция getopt вернет ?, если есть нераспознанная опция, которую она сохранит во внешней переменной optopt.

□ Если опции требуется значение (например, в нашем примере опции -f) и не задана никакая величина, getopt обычно возвращает ?. Если поместить двоеточие как первый символ в строке опций, при отсутствии заданной величины функция getopt вернет : вместо ?.

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

Некоторые версии функции getopt прекратят выполнение при обнаружении первого аргумента не опции, вернув значение -1 и установив переменную optind. Другие, например предлагаемые в ОС Linux, могут обрабатывать опции, где бы они ни встретились в аргументах программы. Учтите, что в данном случае getopt фактически перепишет массив argv так, что все аргументы не опции будут собраны вместе, начиная с элемента массива argv[optind]. В случае версии GNU функции getopt ее поведение определяется переменной окружения POSIXLY_CORRECT. Если переменная установлена, getopt остановится на первом аргументе не опции. Кроме того, некоторые реализации getopt выводят сообщения об ошибке для незнакомых опций. Имейте в виду, что в стандарте POSIX написано о том, что если переменная opterr не равна нулю, функция getopt выведет сообщение об ошибке в stderr.

Итак, выполните упражнение 4.2.

Упражнение 4.2. Функция getopt 

В этом упражнении вы используете функцию getopt; назовите новую программу argopt.c.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

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

 int opt;

 while ((opt = getopt(argc, argv, ":if:lr")) != -1) {

  switch(opt) {

  case 'i':

  case 'l':

  case 'r':

   printf("option: %c\n", opt);

   break;

  case 'f':

   printf("filename: %s\n", optarg);

   break;

  case ':':

   printf("option needs a value\n");

   break;

  case '?':

   printf("unknown option: %c\n", optopt);

   break;

  }

 }

 for (; optind < argc; optind++)

  printf("argument: %s\n", argv[optind]);

 exit(0);

}

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

$ ./argopt -i -lr 'hi there' -f fred.с -q

option: i

option: l

option: r

filename: fred.c

unknown option: q

argument: hi there

Как это работает

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

Когда все опции обработаны, программа просто выводит оставшиеся аргументы, как и раньше, но начиная с номера, хранящегося в переменной optind.

getopt_long

Многие приложения Linux принимают более информативные аргументы, чем использованные в предыдущем примере односимвольные опции. Библиотека С проекта GNU содержит версию функции getopt, названную getopt_long, которая принимает так называемые длинные аргументы, которые вводятся с помощью двойного дефиса.

Рассмотрим упражнение 4.3.

Упражнение 4.3. Функция getopt_long

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

$ ./longopt --initialize --list 'hi there' --file fred.c -q

option: i

option: l

filename: fred.c

./longopt: invalid option --q

unknown option: q

argument: hi there

На самом деле и новые длинные опции, и исходные односимвольные можно смешивать. Длинным опциям также можно давать сокращенные названия, но они

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

$ ./longopt --init -l --file=fred.с 'hi there'

option: i

option: l

filename: fred.с

argument: hi there

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

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#define _GNU_SOURCE

#include <getopt.h>

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

 int opt;

 struct option_longopts[] = {

  {"initialize", 0. NULL, 'i'},

  {"file" 1, NULL, 'f'},

  {"list", 0, NULL, 'l'},

 {0, 0, 0, 0}};

 while ((opt = getopt_long(argc, argv, ":if:lr, longopts, NULL)) != -1) {

  switch(opt) {

  case 'i':

  case 'l':

  case 'r':

   printf("option: %c\n", opt);

   break;

  case 'f':

   printf("filename: %s\n", optarg);

   break;

  case ':':

   printf("option needs a value\n");

   break;

  case '?':

   printf("unknown option: %c\n", optopt);

   break;

  }

 }

 for (; optind < argc; optind++)

  printf("argument: %s\n", argv[optind]);

 exit(0);

}

Как это работает

Функция getopt_long принимает два дополнительных параметра по сравнению с функцией getopt. Первый из них — массив структур, описывающий длинные опции и сообщающий функции getopt_long способ их обработки. Второй дополнительный параметр — адрес переменной, которая может использоваться как вариант optind, предназначенный для длинных опций; для каждой распознанной длинной опции ее номер в массиве длинных опций может быть записан в эту переменную. В данном примере вам не нужна эта информация, поэтому вы используете NULL в качестве значения второго дополнительного параметра.

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

Структура длинной опции определена в заголовочном файле getopt.h и должна подключаться с помощью константы _GNU_SOURCE, определенной для того, чтобы разрешить использование функции getopt_long.

struct option {

 const char *name;

 int has_arg;

 int *flag;

 int val;

};

Элементы структуры описаны в табл. 4.1.

Таблица 4.1.

Параметр опции Описание
name Название длинной опции. Сокращения будут приниматься до тех пор, пока они не создадут путаницы при определении названий других опций
has_arg Принимает ли эта опция аргумент. Задайте 0 для опций без аргументов, 1 для опций, у которых должно быть значение, и 2 для опций с необязательным аргументом
flag Задайте NULL, чтобы getopt_long вернула при обнаружении данной опции значение, заданное в val. В противном случае getopt_long возвращает 0 и записывает значение val в переменную, на которую указывает flag
val Значение getopt_long для данной опции, предназначенное для возврата

Для получения сведений о других опциях, связанных с расширениями функции getopt в проекте GNU и родственных функциях, см. страницы интерактивного справочного руководства к функции getopt.

Переменные окружения

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

$ echo $НOМЕ

/home/neil

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

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

#include <stdlib.h>

char *getenv(const char *name);

int putenv(const char *string);

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

Функция putenv принимает строку вида имя=значение и добавляет ее в текущее окружение. Она даст сбой и вернет -1, если не сможет расширить окружение из-за нехватки свободной памяти. Когда это произойдет, переменной errno будет присвоено значение ENOMEM.

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

Упражнение 4.4. Функции getenv и putenv

1. Первые несколько строк после объявления функции main гарантируют корректный вызов программы environ.c с только одним или двумя аргументами:

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

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

 char *var, *value;

 if (argc == 1 || argc > 3) {

  fprintf(stderr, "usage: environ var [value]\n");

  exit(1);

 }

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

 var = argv[1];

 value = getenv(var);

 if (value)

  printf("Variable %s has value %s\n", var, value);

 else

  printf("Variable %s has no value\n", var);

3. Далее проверьте, был ли при вызове программы указан второй параметр. Если был, вы задаете значение этого аргумента, конструируя строку вида имя=значение и затем вызывая функцию putenv:

 if (argc == 3) {

  char *string;

  value = argv[2];

  string = malloc(strlen(var)+strlen(value)+2);

  if (!string} {

   fprintf(stderr, "out of memory\n");

   exit(1);

  }

  strcpy(string, var);

  strcat(string, "=");

  strcat(string, value);

  printf("Calling putenv with: %s\n", string);

  if (putenv(string) != 0) {

   fprintf(stderr, "putenv failed\n");

   free(string);

   exit(1);

  }

4. В заключение вы узнаете новое значение переменной, вызвав функцию getenv еще раз:

  value = getenv(var);

  if (value)

   printf("New value of %s is %s\n", var, value);

  else

   printf("New value of %s is null??\n", var);

 }

 exit(0);

}

Когда вы выполните эту программу, то сможете увидеть и задать переменные окружения:

$ ./environ НОМЕ

Variable HOME has value /home/neil

$ ./environ FRED

Variable FRED has no value

$ ./environ FRED hello

Variable FRED has no value

Calling putenv with: FRED=hello

New value of FRED is hello

$ ./environ FRED

Variable FRED has no value

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

Применение переменных окружения

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

$ ./environ FRED

Variable FRED has no value

$ FRED=hello ./environ FRED

Variable FRED has value hello

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

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

$ CDDB=mycds; export CDDB

$ cdapp

или

$ CDDB=mycds cdapp

Примечание

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

Переменная environ

Как вы уже знаете, окружение программы формируется из строк вида имя=значение. Этот массив строк становится доступен программе непосредственно из переменной environ, которая объявляется, как

#include <stdlib.h>

extern char **environ;

Выполните упражнение 4.5.

Упражнение 4.5. Переменная environ

Далее приведена программа showenv.c, использующая переменную environ для вывода переменных окружения.

#include <stdlib.h>

#include <stdio.h>

extern char **environ;

int main() {

 char **env = environ;

 while (*env) {

  printf("%s\n", *env);

  env++;

 }

 exit(0);

}

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

$ ./showenv

HOSTNAME=tilde.provider.com

LOGNAME=neil

MAIL=/var/spool/mail/neil

TERM=xterm

HOSTTYPE=i386

PATH=/usr/local/bin:/bin:/usr/bin:

HOME=/usr/neil

LS_OPTIONS=-N --color=tty -T 0

SHELL=/bin/bash

OSTYPE=Linux

...

Как это работает

Для вывода всего окружения программа в цикле обращается к переменной environ — массиву нуль-терминированных строк.

Время и дата

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

Примечание

Во всех системах UNIX применяется одна и та же точка отсчета времени и дат: полночь по Гринвичу (GMT) на 1 января 1970 г. Это "начало эпохи UNIX", и ОС Linux — не исключение. Время в системе Linux измеряется в секундах, начиная с этого момента времени. Такой способ обработки аналогичен принятому в системе MS-DOS за исключением того, что эпоха MS-DOS началась в 1980 г. В других системах применяют точки отсчета иных эпох.

Время задается с помощью типа time_t. Это целочисленный тип, достаточный для хранения дат и времени в секундах. В Linux-подобных системах это тип long integer (длинное целое), определенный вместе с функциями, предназначенными для обработки значений времени, в заголовочном файле time.h.

Примечание

Не думайте, что для хранения времени достаточно 32 битов. В системах UNIX и Linux, использующих 32-разрядный тип time_t, временное значение "будет превышено" в 2038 г. Мы надеемся, что к тому времени системы перейдут на тип time_t, содержащий более 32 битов. Недавнее широкое внедрение 64-разрядных процессоров превращает это практически в неизбежность.

#include <time.h>

time_t time(time_t *tloc);

Вы можете найти низкоуровневое значение времени, вызвав функцию time, которая вернет количество секунд с начала эпохи (упражнение 4.6). Она также запишет возвращаемое значение по адресу памяти, на который указывает параметр tloc, если он — непустой указатель.

Упражнение 4. Функция time

Далее для демонстрации функции time приведена простая программа envtime.c.

#include <time.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int i;

 time_t the_time;

 for (i = 1; i <= 10; i++) {

  the_time = time((time_t *)0);

  printf("The time is %ld\n", the_time);

  sleep(2);

 }

 exit(0);

}

Когда вы запустите программу, она будет выводить низкоуровневое значение времени каждые 2 секунды в течение 20 секунд.

$ ./anytime

The time is 1179643852

The time is 1179643854

The time is 1179643856

The time is 1179643858

The time is 1179643860

The time is 1179643862

The time is 1179643864

The time is 1179643866

The time is 1179643868

The time is 1179643870

Как это работает

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

Использование времени и даты в виде количества секунд, прошедших с начала 1970 г., может быть полезно для измерения длительности чего-либо. Вы сможете сосчитать простую разность значений, полученных из двух вызовов функции time. Однако комитет, разрабатывавший стандарт языка ISO/ANSI С, в своих решениях не указал, что тип time_t будет применяться для определения произвольных интервалов времени в секундах, поэтому была придумана функция difftime, которая вычисляет разность в секундах между двумя значениями типа time_t и возвращает ее как величину типа double:

#include <time.h>

double difftime(time_t time1, time_t time2);

Функция difftime вычисляет разницу между двумя временными значениями и возвращает величину, эквивалентную выражению время1–время2, как число с плавающей точкой. В ОС Linux значение, возвращаемое функцией time, — это количество секунд, которое может обрабатываться, но для максимальной переносимости следует применять функцию difftime.

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

Функция gmtime подразделяет низкоуровневое значение времени на структуру, содержащую более привычные поля:

#include <time.h>

struct tm *gmtime(const time_t timeval)

В структуре tm, как минимум, определены элементы, перечисленные в табл. 4.2.

Таблица 4.2

Элемент tm Описание
int tm_sec Секунды, 0–61
int tm_min Минуты, 0–59
int tm_hour Часы, 0–23
int tm_mday День в месяце, 1–31
int tm_mon Месяц в году, 0–11 (January (январь) соответствует 0)
int tm_year Годы, начиная с 1900 г.
int tm_wday День недели, 0–6 (Sunday (воскресенье) соответствует 0)
int tm_yday День в году, 0–365
int tm_isdst Действующее летнее время

Диапазон элемента tm_sec допускает появление время от времени корректировочной секунды или удвоенной корректировочной секунды.

Выполните упражнение 4.7.

Упражнение 4.7. Функция gmtime

Далее приведена программа gmtime.с, выводящая текущие время и дату с помощью структуры tm и функции gmtime.

#include <time.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 struct tm *tm_ptr;

 time_t the_time;

 (void)time(&the_time);

 tm_ptr = gmtime(&the_time);

 printf("Raw time is %ld\n", the_time);

 printf("gmtime gives:\n");

 printf("date: %02d/%02d/%02d\n",

tm_ptr->tm_year, tm_ptr->tm_mon+1, tm_ptr->tm_mday);

 printf("time: %02d:%02d:%02d\n",

  tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec);

 exit(0);

}

Выполнив эту программу, вы получите хорошее соответствие текущим времени и дате:

$ ./gmtime; date

Raw time is 1179644196

gmtime gives:

date: 107/05/20

time: 06:56:36

Sun May 20 07:56:37 BST 2007

Как это работает

Программа вызывает функцию time для получения машинного представления значения времени и затем вызывает функцию gmtime для преобразования его в структуру с удобными для восприятия значениями времени и даты. Она выводит на экран полученные значения с помощью функции printf. Строго говоря, выводить необработанное значение времени таким способом не следует, потому что наличие типа длинного целого не гарантировано во всех системах. Если сразу же после вызова функции gmtime выполнить команду date, можно сравнить оба вывода.

Но здесь у вас возникнет небольшая проблема. Если вы запустите эту программу в часовом поясе, отличном от Greenwich Mean Time (время по Гринвичу) или у вас действует летнее время, как у нас, вы заметите, что время (и, возможно, дата) неправильное. Все дело в том, что функция gmtime возвращает время по Гринвичу (теперь называемое Universal Coordinated Time (всеобщее скоординированное время) или UTC). Системы Linux и UNIX поступают так для синхронизации всех программ и систем в мире. Файлы, созданные в один и тот же момент в разных часовых поясах, будут отображаться с одинаковым временем создания. Для того чтобы посмотреть местное время, следует применять функцию localtime.

#include <time.h>

struct tm *localtime(const time_t *timeval);

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

Для преобразования разделенной на элементы структуры tm в общее внутреннее значение времени можно применить функцию mktime:

#include <time.h>

time_t mktime(struct tm *timeptr);

Функция mktime вернет -1, если структура не может быть представлена как значение типа time_t.

Для вывода программой date "дружественных" (в противоположность машинному) времени и даты можно воспользоваться функциями asctime и ctime:

#include <time.h>

char *asctime(const struct tm *timeptr);

char *ctime(const time_t *timeval);

Функция asctime возвращает строку, представляющую время и дату, заданные tm-структурой timeptr. У возвращаемой строки формат, подобный приведенному далее:

Sun Jun  9 12:34:56 2007\n\0

У нее всегда фиксированный формат длиной 26 символов. Функция ctime эквивалентна следующему вызову:

asctime(localtime(timeval))

Она принимает необработанное машинное значение времени и преобразует его в местное время.

А теперь выполните упражнение 4.8.

Упражнение 4.8. Функция ctime

В этом примере благодаря приведенному далее программному коду вы увидите функцию ctime в действии.

#include <time.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 time_t timeval;

 (void)time(&timeval);

 printf ("The date is: %s", ctime(&timeval));

 exit(0);

}

Откомпилируйте и затем запустите на выполнение ctime.c, и вы увидите нечто похожее на приведенные далее строки:

$ ./ctime

The date is: Sat Jun 9 08:02:08 2007.

Как это работает

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

Для лучшего управления точным форматированием времени и даты ОС Linux и современные UNIX-подобные системы предоставляют функцию strftime. Она довольно похожа на функцию sprintf для дат и времени и действует аналогичным образом:

#include <time.h>

size_t strftime(char *s, size_t maxsize, const char *format, struct tm *timeptr);

Функция strftime форматирует время и дату, представленные в структуре tm, на которую указывает параметр, timeptr, и помещает результат в строку s. Эта строка задается длиной maxsize (как минимум) символов. Строка format применяется для управления символами, записываемыми в строку. Как и в функции printf, она содержит обычные символы, которые будут переданы в строку, и спецификаторы преобразований для форматирования элементов времени и даты. В табл. 4.3 перечислены используемые спецификаторы преобразований.

Таблица 4.3

Спецификатор преобразования Описание
%a Сокращенное название дня недели
Полное название дня недели
%b Сокращенное название месяца
%B Полное название месяца
%c Дата и время
%d День месяца, 01–31
%H Час, 00–23
%I Час по 12-часовой шкале, 01–12
%j День в году, 001–366
%m Номер месяца в году, 01–12
%M Минуты, 00–59
%p a.m. (до полудня) или p.m. (после полудня)
%S Секунды, 00–59
%u Номер дня недели, 1–7 (1 соответствует понедельнику)
%U Номер недели в году, 01–53 (воскресенье — первый день недели)
%V Номер недели в году, 01–53 (понедельник — первый день недели)
%w Номер дня недели, 0–6 (0 соответствует воскресенью)
%x Дата в региональном формате
%X Время в региональном формате
%y Номер года, меньший 1900
%Y Год
%Z Название часового пояса
%% Символ %

Таким образом, обычная дата, такая же, как полученная из программы date, соответствует следующей строке формата функции strftime:

"%a %b %d %Н: %М: %S %Y"

Для облегчения чтения дат можно использовать функцию strptime, принимающую строку с датой и временем и формирующую структуру tm с теми же датой и временем:

#include <time.h>

char *strptime(const char *buf, const char *format, struct tm *timeptr);

Строка format конструируется точно так же, как одноименная строка функции strftime. Функций strptime действует аналогично функции sscanf: она сканирует строку в поиске опознаваемых полей и записывает их в переменные. В данном случае это элементы структуры tm, которая заполняется в соответствии со строкой format. Однако спецификаторы преобразований для strptime немного мягче спецификаторов функции strftime. Так, в функции strptime разрешены как сокращенные, так и полные названия дней и месяцев. Любое из этих представлений будет соответствовать спецификатору %a функции strptime. Кроме того, в то время как функция strftime для представления чисел, меньших 10, всегда применяет ведущие нули, strptime считает их необязательными.

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

Рассмотрим работу функций на примере (упражнение 4.9).

Упражнение 4.9. Функции strftime и strptime

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

#include <time.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 struct tm *tm_ptr, timestruct;

 time_t the_time;

 char buf[256];

 char *result;

 (void)time(&the_time);

 tm_ptr = localtime(&the_time);

 strftime(buf, 256, "%A %d %B, %I:%S %p", tm_ptr);

 printf("strftime gives: %s\n", buf);

 strcpy(buf, "Thu 26 July 2007, 17:53 will do fine");

 printf("calling strptime with: %s\n", buf);

 tm_ptr = &timestruct;

 result = strptime(buf, "%a %d %b %Y, %R", tm_ptr);

 printf("strptime consumed up to: %s\n", result);

 printf("strptime gives:\n");

 printf ("date: %02d/%02d/%02d\n",

  tm_ptr->tm_year % 100, tm_ptr->tm_mon+1, tm_ptr->tm_mday);

 printf("time: %02d:%02d\n",

  tm_ptr->tm_hour, tm->ptr->tm_min);

 exit(0);

}

Когда вы откомпилируете и выполните программу strftime.c, то получите следующий результат:

$ ./strftime

strftime gives: Saturday 09 June, 08:16 AM

calling strptime with: Thu 26 July 2007, 17:53 will do fine

strptime concurred up to: will do fine

strptime gives:

date: 07/07/26

time: 17:53

Как это работает

Программа strftime получает текущее местное время с помощью вызовов функций time и localtime. Затем она преобразует его в удобочитаемую форму с помощью функции strftime с подходящим аргументом форматирования. Для демонстрации применения функции strptime программа задает строку, содержащую дату и время, затем вызывает strptime для извлечения необработанных значений времени и даты и выводит их на экран. Спецификатор преобразования %R функции strptime — это сокращенное обозначение комбинации %Н:%M.

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

Возможно, при компиляции программы strftime.c вы получите предупреждение компилятора. Причина в том, что по умолчанию в библиотеке GNU не объявлена функция strptime. Для устранения проблемы следует явно запросить средства стандарта X/Open, добавив следующую строку перед заголовочным файлом time.h:

#define _XOPEN_SOURCE

Временные файлы

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

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

Уникальное имя файла генерируется с помощью функции tmpnam:

#include <stdio.h>

char *tmpnam(char *s);

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

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

#include <stdio.h>

FILE* tmpfile(void);

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

В случае возникновения ошибки tmpfile вернет указатель NULL и задаст значение переменной errno.

Давайте посмотрим эти две функции в действии:

#include <stdio.h>

#include <stdlib.h>

int main() {

 char tmpname[L_tmpnam];

 char* filename;

 FILE *tmpfp;

 filename = tmpnam(tmpname);

 printf("Temporary file name is: %s\n", filename);

 tmpfp = tmpfile();

 if (tmpfp) printf("Opened a temporary file OK\n");

 else perror("tmpfile");

 exit(0);

}

Когда вы откомпилируете и выполните программу tmpnam.с, то увидите уникальное имя файла, сгенерированное функцией tmpnam:

$ ./tmpnam

Temporary file name is: /tmp/file2S64zc

Opened a temporary file OK

Как это работает

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

В некоторых версиях UNIX предлагается другой способ генерации имен временных файлов — с помощью функций mktemp и mkstemp. Они поддерживаются и ОС Linux и аналогичны функции tmpnam за исключением того, что вы должны задать шаблон имени временного файла, который предоставляет некоторые дополнительные возможности управления местом хранения и именем файла.

#include <stdlib.h>

char *mktemp(char *template);

int mkstemp(char *template);

Функция mktemp создает уникальное имя файла на основе заданного шаблона template. Аргумент template должен быть строкой с шестью завершающими символами Х. mktemp заменяет эти символы Х уникальной комбинацией символов, допустимых в именах файлов. Она возвращает указатель на сгенерированную строку или NULL при невозможности сформировать уникальное имя.

Функция mkstemp аналогична функции tmpfile: она создает и открывает временный файл. Имя файла генерируется так же, как в функции mktemp, но возвращенный результат — открытый низкоуровневый дескриптор файла.

Примечание

В ваших собственных программах следует всегда применять функции "создать и открыть" tmpfile и mkstemp вместо функций tmpnam и mktemp.

Информация о пользователе

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

Когда пользователь регистрируется в системе Linux, у него или у нее есть имя пользователя и пароль. После того как эти данные проверены, пользователю предоставляется командная оболочка. В системе у пользователя также есть уникальный идентификатор пользователя, называемый UID (user identifier). Каждая программа, выполняемая Linux, запускается от имени пользователя и имеет связанный с ней UID.

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

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

У UID есть свои тип uid_t, определенный в файле sys/types.h. Обычно это короткое целое (small integer). Одни идентификаторы пользователя заранее определены системой, другие создаются системным администратором, когда новые пользователи становятся известны системе. Как правило, идентификаторы пользователей имеют значения, большие 100.

#include <sys/types.h>

#include <unistd.h>

uid_t getuid (void);

char *getlogin(void);

Функция getuid возвращает UID, с которым связана программа. Обычно это UID пользователя, запустившего программу.

Функция getlogin возвращает регистрационное имя, ассоциированное с текущим пользователем.

Системный файл /etc/passwd содержит базу данных, имеющую дело с учетными записями пользователей. Он состоит из строк по одной на каждого пользователя, в каждую строку включены имя пользователя, зашифрованный пароль, идентификатор пользователя (UID), идентификатор группы (GID), полное имя, исходный каталог и командная оболочка, запускаемая по умолчанию. Далее приведен пример такой строки:

neil:zBqxfqedfpk:500:100:Neil Matthew:/home/neil:/bin/bash

Если вы пишете программу, которая определяет UID пользователя, запустившего ее, то можете расширить ее возможности и заглянуть в файл passwd для выяснения регистрационного имени пользователя и его полного имени. Мы не рекомендуем делать это, потому что современные UNIX-подобные системы уходят от применения файлов учетных записей пользователей для повышения безопасности системы. Многие системы, включая Linux, имеют возможность использовать файлы теневых паролей (shadow password), совсем не содержащие пригодной информации о зашифрованных паролях (она часто хранится в файле /etc/shadow, которые обычные пользователи не могут читать). По этой причине определен ряд функций для предоставления эффективного программного интерфейса, позволяющего получать эту пользовательскую информацию.

#include <sys/types.h>

#include <pwd.h>

struct passwd *getpwuid(uid_t uid);

struct passwd *getpwnam(const char *name);

Структура базы данных учетных записей пользователей passwd определена в файле pwd.h и включает элементы, перечисленные в табл. 4.4.

Таблица 4.4

Элемент passwd Описание
char *pw_name Регистрационное имя пользователя
uid_t pw_uid Номер UID
gid_t pw_gid Номер GID
char *pw_dir Исходный каталог пользователя
char *pw_gecos Полное имя пользователя
char *pw_shell Командная оболочка пользователя, запускаемая по умолчанию

В некоторых системах UNIX может использоваться другое имя для поля с полным именем пользователя: в одних системах это pw_gecos, как в ОС Linux, в других — pw_comment. Это означает, что мы не можем рекомендовать его использование. Обе функции (и getpwuid, и getpwnam) возвращают указатель на структуру passwd, соответствующую пользователю. Пользователь идентифицируется по UID в функции getpwuid и по регистрационному имени в функции getpwnam. В случае ошибки обе функции вернут пустой указатель и установят переменную errno.

Выполните упражнение 4.11.

Упражнение 4.11. Информации о пользователе

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

#include <sys/types.h>

#include <pwd.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 uid_t uid;

 gid_t gid;

 struct passwd *pw;

 uid = getuid();

 gid = getgid();

 printf("User is %s\n", getlogin());

 printf("User IDs: uid=%d, gid=%d\n", uid, gid);

 pw = getpwuid(uid);

 printf(

  "UID passwd entry:\n name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",

  pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);

 pw = getpwnam("root");

 printf("root passwd entry:\n");

 printf("name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",

  pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);

 exit(0);

}

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

$ ./user

User is neil

User IDs: uid=1000, gid=100

UID passwd entry:

name=neil, uid=1000, gid=100, home=/home/neil, shell=/bin/bash

root passwd entry:

name=root, uid=0, gid=0, home=/root, shell=/bin/bash

Как это работает

Эта программа вызывает функцию getuid для получения UID текущего пользователя, Этот UID применяется в функции getpwuid для получения подробной информации из файла учетных записей пользователей. В качестве альтернативы мы показываем, как для извлечения информации о пользователе можно задать в функции getpwnam имя пользователя root.

Примечание

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

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

#include <pwd.h>

#include <sys/types.h>

void endpwent(void);

struct passwd *getpwent(void);

void setpwent(void);

Функция getpwent возвращает поочередно информацию о каждом пользователе. Когда не остается ни одного, она возвращает пустой указатель. Для прекращения обработки файла, когда просмотрено достаточно элементов, вы можете применить функцию endpwent. Функция setpwent переустанавливает позицию указателя в файле учетных записей пользователей для начала нового просмотра при следующем вызове функции getpwent. Эти функции действуют так же, как функции просмотра каталога opendir, readdir и closedir, обсуждавшиеся в главе 3.

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

#include <sys/types.h>

#include <unistd.h>

uid_t geteuid(void);

gid_t getgid(void);

gid_t getegid(void);

int setuid(uid_t uid);

int setgid(gid_t gid);

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

Примечание

Только суперпользователь может вызывать функции setuid и setgid.

Информация о компьютере

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

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

Если в системе установлены сетевые компоненты, вы очень легко можете получить сетевое имя компьютера с помощью функции gethostname:

#include <unistd.h>

int gethostname(char *name, size_t namelen);

Эта функция записывает сетевое имя машины в строку name. Предполагается, что длина строки, как минимум, namelen символов. Функция gethostname возвращает 0 в случае успешного завершения и -1 в противном случае.

Более подробную информацию о рабочем компьютере можно получить с помощью системного вызова uname.

#include <sys/utsname.h>

int uname(struct utsname *name);

Функция uname записывает информацию о компьютере в структуру, на которую указывает параметр name. Структура типа utsname, определенная в файле sys/utsname.h, обязательно должна включать элементы, перечисленные в табл. 4.5.

Таблица 4.5

Элемент структуры utsname Описание
char sysname[] Имя операционной системы
char nodename[] Имя компьютера
char release[] Номер выпуска (релиза) системы
char version[] Номер версии системы
char machine[] Аппаратный тип

В случае успешного завершения функция uname возвращает неотрицательное целое и в противном случае с установленной переменной errno для обозначения любой возникшей ошибки.

Выполните упражнение 4.12.

Упражнение 4.12. Информации о компьютере

Далее приведена программа hostget.c, извлекающая некоторые сведения о рабочем компьютере.

#include <sys/utsname.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 char computer[256];

 struct utsname uts;

 if (gethostname(computer, 255) != 0 || uname(&uts) < 0) {

  fprintf(stderr, "Could not get host information\n");

  exit(1);

 }

 printf("Computer host name is %s\n", computer);

 printf("System is %s on %s hardware\n", uts.sysname, uts.machine);

 printf("Nodename is %s\n", uts.nodename);

 printf("Version is %s, %s\n", uts.release, uts.version);

 exit(0);

}

Она отобразит следующие зависящие от ОС Linux данные. Если ваша машина включена в сеть, то вы увидите расширенное имя компьютера, включающее обозначение сети:

$ ./hostget

Computer host name is suse103

System is Linux on i686 hardware

Nodename is suse103

Version is 2.6.20.2-2-default, #1 SMP Fri Mar 9 21:54:10 UTC 2007

Как это работает

Эта программа вызывает функцию gethostname для получения имени рабочего компьютера. В приведенном примере это имя — suse103. Более подробную информацию об этом компьютере на базе Intel Pentium 4 с ОС Linux возвращает системный вызов uname. Учтите, что формат возвращаемых строк зависит от реализации, например, строка с версией системы содержит дату компиляции ядра.

Примечание

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

Уникальный идентификатор каждого рабочего компьютера можно получить с помощью функции gethostid.

#include <unistd.h>

long gethostid(void);

Функция gethostid предназначена для возврата уникального значения, характеризующего рабочий компьютер. Менеджеры, следящие за соблюдением лицензионных соглашений, применяют ее для того, чтобы обеспечить функционирование программного обеспечения только на машинах с действующими лицензиями. На рабочих станциях Sun она возвращает номер, установленный в постоянной памяти во время изготовления компьютера и, таким образом, уникальный для системного оборудования. Другие системы, например Linux, возвращают значение на базе интернет-адреса машины, обычно не слишком безопасного для проверки лицензионных прав.

Ведение системных журналов

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

Очень часто зарегистрированные сообщения записываются в системные файлы в каталоге, предоставляемом для этой цели. Это может быть каталог /usr/admor/var/log. При типичной установке ОС Linux все системные сообщения содержатся в файле /var/log/messages, в файл /var/log/mail включены другие регистрируемые сообщения от почтовой системы, а в файле /var/log/debug могут храниться отладочные сообщения. Проверить конфигурацию своей системы можно в файле /etc/syslog.conf или /etc/syslog-ng/syslog-ng.conf в зависимости от версии Linux.

Далее приведены некоторые примеры зарегистрированных сообщений.

Mar 2 6 18:25:51 suse103 ifstatus: eth0 device: Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] (rev 10)

Mar 26 18:25:51 suse103 ifstatus: eth0 configuration: eth-id-00:0c:29:0e:91:72

...

May 20 06:56:56 suse103 SuSEfirewall2: Setting up rules from /etc/sysconfig/SuSEfirewall2

...

May 20 06:56:57 suse103 SuSEfirewall2: batch committing

...

May 20 06:56:57 suse103 SuSEfirewall2: Firewall rules successfully set

...

Jun 9 09:11:14 suse103 su: (to root) neil on /dev/pts/18 09:50:35

В этом выводе показаны виды регистрируемых сообщений. Несколько первых отправлены непосредственно ядром Linux во время его загрузки и обнаружения установленного оборудования. Брандмауэр сообщает о своей перенастройке. И наконец, программа su извещает о том, что доступ с учетной записью суперпользователя получен пользователем neil.

Примечание

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

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

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

#include <syslog.h>

void syslog(int priority, const char *message, arguments...);

Функция syslog посылает регистрируемое сообщение средству ведения системного журнала (logging facility). У каждого сообщения есть аргумент priority, полученный поразрядной операцией OR из степени важности сообщения (severity level) и типа программы, формирующей сообщение (facility value). Степень важности определяет необходимые действия, а тип программы фиксирует инициатора сообщения.

Типы программ (из файла syslog.h) включают константу LOG_USER, применяемую для обозначения сообщения, пришедшего из приложения пользователя (по умолчанию), и константы LOG_LOCAL0, LOG_LOCAL1, ..., LOG_LOCAL7, зарезервированные для локального администратора.

В табл. 4.6 перечислены степени важности сообщений в порядке убывания приоритета.

Таблица 4.6

Приоритет Описание
LOG_EMERG Кризисная ситуация
LOG_ALERT Проблема с высоким приоритетом, например, повреждение базы данных
LOG_CRIT Критическая ошибка, например, повреждение оборудования
LOG_ERR Ошибки
LOG_WARNING Предупреждение
LOG_NOTICE Особые обстоятельства, требующие повышенного внимания
LOG_INFO Информационные сообщения
LOG_DEBUG Отладочные сообщения

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

У сообщения, создаваемого syslog, есть заголовок и тело сообщения. Заголовок создается из индикатора типа программы, формирующей сообщение, и даты и времени. Тело сообщения создается из параметра message, передаваемого функции syslog, который действует как строка format функции printf. Остальные аргументы syslog используются в соответствии со спецификаторами преобразований в стиле функции printf, заданными в строке message. Дополнительно может применяться спецификатор %m для включения строки сообщения об ошибке, ассоциированной с текущим значением переменной errno. Эта возможность может оказаться полезной для регистрации сообщений об ошибках.

Выполните упражнение 4.13.

Упражнение 4.13. Применение функции syslog

В этой программе осуществляется попытка открыть несуществующий файл.

#include <syslog.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 FILE *f;

 f = fopen("not_here", "r");

 if (!f) syslog(LOG_ERR|LOG_USER, "oops - %m\n");

 exit(0);

}

Когда вы откомпилируете и выполните программу syslog.с, то не увидите никакого вывода, но в конце файла /var/log/messages теперь содержится следующая строка:

Jun 9 09:24:50 suse103 syslog: oops — No such file or directory

Как это работает

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

Обратите внимание на то, что регистрируемое сообщение не указывает, какая программа вызвала средство регистрации; оно просто констатирует тот факт, что была вызвана функция syslog с сообщением. Спецификатор преобразования %m был заменен описанием ошибки, в данном случае сообщающим об отсутствии файла. Это гораздо полезнее, чем простой отчет, содержащий внутренний номер ошибки.

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

К ним относятся следующие функции:

#include <syslog.h> void closelog(void);

void openlog(const char *ident, int logopt, int facility);

int setlogmask(int maskpri);

Вы можете изменить способ представления ваших регистрируемых сообщений, вызвав функцию openlog. Это позволит задать строку ident, которая будет добавляться к вашим регистрируемым сообщениям. Вы можете применять ее для индикации программы, создавшей сообщение. Параметр facility записывает текущий принятый по умолчанию тип программы, формирующей сообщение, который будет использоваться в последующих вызовах syslog. По умолчанию устанавливается значение LOG_USER. Параметр logopt настраивает поведение будущих вызовов функции syslog. Он представляет собой результат поразрядной операции OR нулевого или большего числа параметров, приведенных в табл. 4.7.

Таблица 4.7

Параметр logopt Описание
LOG_PID Включает в сообщения идентификатор процесса, уникальный номер, выделяемый системой каждому процессу
LOG_CONS Посылает сообщения на консоль, если они не могут быть записаны
LOG_ODELAY Открывает средство регистрации сообщений при первом вызове функции syslog
LOG_NDELAY Открывает средство регистрации сообщений немедленно, не дожидаясь первого регистрируемого сообщения

Функция openlog выделит и откроет дескриптор файла, который будет применяться для записи в программе ведения системного журнала. Вы сможете закрыть его, вызвав функцию closelog. Имейте в виду, что вам не нужно вызывать функцию openlog перед вызовом syslog, потому что последняя при необходимости самостоятельно откроет средство ведения системного журнала.

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

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

Выполните упражнение 4.14.

Упражнение 4.14. Маска регистрации (logmask)

В этом примере вы увидите logmask в действии.

#include <syslog.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int logmask;

 openlog("logmask", LOG_PID|LOG_CONS, LOG_USER);

 syslog(LOG_INFO, "informative message, pid = %d", getpid());

 syslog(LOG_DEBUG, "debug message, should appear");

 logmask = setlogmask(LOG_UPTO(LOG_NOTICE));

 syslog(LOG_DEBUG, "debug message, should not appear");

exit(0);

}

Программа logmask.c ничего не выводит, но в типичной системе Linux вы увидите в файле /var/log/messages, ближе к концу, следующую строку:

Jun 9 09:28:52 suse103 logmask[19339] : informative message, pid = 19339

Файл, настроенный на получение регистрируемых сообщений об отладке (в зависимости от настройки регистрации, это чаще всего файл /var/log/debug или иногда файл /var/log/messages), должен содержать следующую строку:

Jun 9 09:28:52 susel03 logmask[19339]: debug message, should appear

Как это работает

Программа инициализирует средство ведения системного журнала, названное logmask, и запрашивает включение идентификатора процесса в регистрируемые сообщения. Информирующее сообщение записывается в файл /var/log/messages, а отладочное сообщение — в файл /var/log/debug. Второе отладочное сообщение не появляется, потому что вы вызвали функцию setlogmask с игнорированием всех сообщений с приоритетом ниже LOG_NOTICE. (Учтите, что этот метод не работает в ранних вариантах ядра Linux.)

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

Программа logmask.c также использует функцию getpid, которая, наряду с тесно связанной с ней функцией getppid, определена следующим образом:

#include <sys/types.h>

#include <unistd.h>

pid_t getpid(void);pid_t getppid(void);

Функции возвращают идентификаторы вызвавшего и родительского процессов. Дополнительную информацию об идентификаторах процессов (PID) см. в главе 11.

Ресурсы и ограничения

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

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

Таблица 4.8

Ограничительная константа Назначение
NAME_MAX Максимальное число символов в имени файла
CHAR_BIT Количество разрядов в значении типа char
CHAR_MAX Максимальное значение типа char
INT_MAX Максимальное значение типа int

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

Примечание

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

В заголовочном файле sys/resource.h представлены определения операций над ресурсами. К ним относятся функции для считывания и установки предельных значений для разрешенного размера программы, приоритета выполнения и файловых ресурсов.

#include <sys/resource.h>

int getpriority(int which, id_t who);

int setpriority(int which, id_t who, int priority);

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

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

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

Здесь id_t — это целочисленный тип, применяемый для идентификаторов пользователя и группы. Структура rusage, указанная в файле sys/resource.h, используется для определения времени центрального процессора (ЦП), затраченного текущей программой. Она должна содержать, как минимум, два элемента (табл. 4.9).

Таблица 4.9

Элемент структуры rusage Описание
struct timeval ru_utime Время, использованное пользователем
struct timeval ru_stime Время, использованное системой

Структура timeval определена в файле sys/time.h и содержит поля tv_sec и tv_usec, представляющие секунды и микросекунды соответственно.

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

Функция getrusage записывает данные о времени ЦП в структуру rusage, на которую указывает параметр r_usage. Параметр who может быть задан одной из констант, приведенных в табл. 4.10.

Таблица 4.10

Константа who Описание
RUSAGE_SELF Возвращает данные о потреблении только для текущей программы
RUSAGE_CHILDREN Возвращает данные о потреблении и для дочерних процессов

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

Примечание

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

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

Таблица 4.11

Параметр which Описание
PRIO_PROCESS who — идентификатор процесса
PRIO_PGRP who — идентификатор группы
PRIO_USER who — идентификатор пользователя

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

priority = getpriority(PRIO_PROCESS, getpid());

Функция setpriority позволяет задать новый приоритет, если это возможно.

По умолчанию приоритет равен 0. Положительные значения приоритета применяются для фоновых задач, которые выполняются, только когда нет задачи с более высоким приоритетом, готовой к выполнению. Отрицательные значения приоритета заставляют программу работать интенсивнее, выделяя большие доли доступного времени ЦП. Диапазон допустимых приоритетов — от -20 до +20. Часто это приводит к путанице, поскольку, чем выше числовое значение, тем ниже приоритет выполнения.

Функция getpriority возвращает установленный приоритет в случае успешного завершения или -1 с переменной errno, указывающей на ошибку. Поскольку значение -1 само по себе обозначает допустимый приоритет, переменную errno перед вызовом функции getpriority следует приравнять нулю и при возврате из функции проверить, осталась ли она нулевой. Функция setpriority возвращает 0 в случае успешного завершения и -1 в противном случае.

Предельные величины, заданные для системных ресурсов, можно прочитать и установить с помощью функций getrlimit и setrlimit. Обе они для описания ограничений ресурсов используют структуру общего назначения rlimit. Она определена в файле sys/resource.h и содержит элементы, перечисленные в табл. 4.12.

Таблица 4.12

Элемент rlimit Описание
rlim_t rlim_cur Текущее, мягкое ограничение
rlim_t rlim_max Жесткое ограничение

Определенный выше тип rlim_t — целочисленный тип, применяемый для описания уровней ресурсов. Обычно мягкое ограничение — это рекомендуемое ограничение, которое не следует превышать; нарушение этой рекомендации может вызвать возврат ошибок из библиотечных функций. При превышении жесткого ограничения система может попытаться завершить программу, отправив ей сигнал, например, сигнал SIGXCPU при превышении ограничения на потребляемое время ЦП и сигнал SIGSEGV при превышении ограничения на объем данных. В программе можно самостоятельно задать для любых значений собственные мягкие ограничения, не превышающие жесткого ограничения. Допустимо уменьшение жесткого ограничения. Увеличить его может только программа, выполняющаяся с правами суперпользователя.

Ограничить можно ряд системных ресурсов. Эти ограничения описаны в параметре resource функций rlimit и определены в файле sys/resource.h, как показано в табл. 4.13.

Таблица 4.13

Параметр resource Описание
RLIMIT_CORE Ограничение размера файла дампа ядра, в байтах
RLIMIT_CPU Ограничение времени ЦП, в секундах
RLIMIT_DATA Ограничение размера сегмента data(), в байтах
RLIMIT_FSIZE Ограничение размера файла, в байтах
RLIMIT_NOFILE Ограничение количества открытых файлов
RLIMIT_STACK Ограничение размера стека, в байтах
RLIMIT_AS Ограничение доступного адресного пространства (стек и данные), в байтах

В упражнении 4.15 показана программа limits.c, имитирующая типичное приложение. Она также задает и нарушает ограничения ресурсов.

Упражнение 4.16. Ограничения ресурсов

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

#include <sys/types.h> \

#include <sys/resource.h>

#include <sys/time.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

2. Функция типа void записывает 10 000 раз строку во временный файл и затем выполняет некоторые арифметические вычисления для загрузки ЦП:

void work() {

 FILE *f;

 int i;

 double x = 4.5;

 f = tmpfile();

 for (i = 0; i < 10000; i++) {

  fprintf(f, "Do some output\n");

  if (ferror(f)) {

   fprintf(stderr, "Error writing to temporary file\n");

   exit(1);

  }

 }

 for (i = 0; i < 1000000; i++) x = log(x*x + 3.21);

}

3. Функция main вызывает функцию work, а затем применяет функцию getrusage для определения времени ЦП, использованного work. Эта информация выводится на экран:

int main() {

 struct rusage r_usage;

 struct rlimit r_limit;

 int priority;

 work();

 getrusage(RUSAGE_SELF, &r_usage);

 printf("CPU usage: User = %ld.%06ld, System = %ld.%06ld\n",

  r_usage.ru_utime.tvsec, rusage.ru_utime.tv_usec,

  r_usage.ru_stime.tv_sec, r_usage.ru_stime.tv_usec);

4. Далее она вызывает функции getpriority и getrlimit для выяснения текущего приоритета и ограничений на размер файла соответственно:

 priority = getpriority(PRIO_PROCESS, getpid());

 printf("Current priority = %d\n", priority);

 getrlimit(RLIMIT_FSIZE, &r_limit);

 printf("Current FSIZE limit: soft = %ld, hard = %ld\n",

  r_limi t.rlim_cur, r_limit.rlim_max);

5. В заключение задайте ограничение размера файла с помощью функции setrlimit и снова вызовите функцию work, которая завершится с ошибкой, т.к. попытается создать слишком большой файл:

 r_limit.rlim_cur = 2048;

 r_limit.rlim_max = 4096;

 printf("Setting a 2K file size limit\n");

 setrlimit(RLIMIT_FS1ZE, &r_limit);

 work();

 exit(0);

}

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

$ cc -о limits limits.с -lm

$ ./limits

CPU usage: User = 0.140008, System = 0.020001

Current priority = 0

Current FSIZE limit: soft = -1, hard = -1

Setting a 2K file size limit

File size limit exceeded

Вы можете изменить приоритет программы, запустив ее с помощью команды nice. Далее показано, как меняется приоритет на значение +10, и в результате программа выполняется немного дольше.

$ nice ./limits

CPU usage: User = 0.152009, System = 0.020001

Current priority = 10

Current FSIZE limit: soft = -1, hard = -1 

Setting a 2K file size limit

File size limit exceeded

Как это работает

Программа limits вызывает функцию work для имитации операций типичной программы. Она выполняет некоторые вычисления и формирует вывод, в данном случае около 150 Кбайт записывается во временный файл. Программа вызывает функции управления ресурсами для выяснения своего приоритета и ограничений на размер файла. В данном случае ограничения размеров файлов не заданы, поэтому можно создавать файл любого размера (если позволяет дисковое пространство). Затем программа задает свое ограничение размера файла, равное примерно 2 Кбайт, и снова пытается выполнить некоторые действия. На этот раз функция work завершается неудачно, поскольку не может создать такой большой временный файл.

Примечание

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

В приведенном примере сообщение об ошибке "Error writing to temporary file" ("Ошибка записи во временный файл") не выводится. Это происходит потому, что некоторые системы (например, Linux 2.2 и более поздние версии) завершают выполнение программы при превышении ограничения ресурса. Делается это с помощью отправки сигнала SIGXFSZ. В главе 11 вы узнаете больше о сигналах и способах их применения. Другие системы, соответствующие стандарту POSIX, заставляют функцию, превысившую ограничение, вернуть ошибку.

Резюме 

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

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

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

Глава 5

Терминалы

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

Несмотря на то, что заново реализованное приложение для управления базой данных компакт-дисков не увидит свет до конца главы 7, его основы вы заложите в этой главе. Глава 6 посвящена curses, которые представляют собой вовсе не древнее проклятие, а библиотеку функций, предлагающих программный код высокого уровня для управления отображением на экране терминала. Попутно вы узнаете чуть больше о размышлениях прежних профи UNIX, познакомившись с основными принципами систем Linux и UNIX и понятием терминала. Низкоуровневый доступ, представленный в этой главе, быть может именно то, что вам нужно. Большая часть того, о чем мы пишем здесь, хорошо подходит для программ, выполняющихся в окне консоли, таких как эмуляторы терминала KDE's Konsole, GNOME's gnome-terminal или стандартный X11 xterm.

В этой главе вы, в частности, узнаете о:

□ чтении с терминала и записи на терминал;

□ драйверах терминала и общем терминальном интерфейсе (General Terminal Interface, GTI);

□ структуре типа termios;

□ выводе терминала и базе данных terminfo;

□ обнаружении нажатия клавиш.

Чтение с терминала и запись на терминал

В главе 3 вы узнали, что, когда программа запускается из командной строки, оболочка обеспечивает присоединение к ней стандартных потоков ввода и вывода. Вы получаете возможность взаимодействия с пользователем простым применением подпрограмм getchar и printf для чтения из стандартного потока ввода и записи в стандартный поток вывода.

В упражнении 5.1 в программе menu1.c вы попытаетесь переписать на языке С подпрограммы формирования меню, использующие только эти две функции.

Упражнение 5.1. Подпрограммы формирования меню на языке C

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

#include <stdio.h>

#include <stdlib.h>

char *menu[] = {

 "a — add new record", "d — delete record", "q - quit", NULL,

};

int getchoice(char *greet, char *choices[]);

2. Функция main вызывает функцию getchoice с образцом пунктов меню menu:

int main() {

 int choice = 0;

 do {

  choice = getchoice("Please select an action", menu);

  printf("You have chosen: %c\n", choice);

 } while (choice != 'q');

 exit(0);

}

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

int getchoice(char *greet, char *choices[]) {

 int chosen = 0;

 int selected;

 char **option;

 do {

  printf("Choice: %s\n", greet);

  option = choices;

  while (*option) {

   printf("%s\n", *option);

   option++;

  }

  selected = getchar();

  option = choices;

  while (*option) {

   if (selected == *option[0]) {

    chosen = 1;

    break;

   }

   option++;

  }

  if (!chosen) {

   printf("Incorrect choice, select again\n");

  }

 } while (!chosen);

 return selected;

}

Как это работает

Функция getchoice выводит на экран приглашение для ввода greet и меню choices и просит пользователя ввести первый символ выбранного пункта. Далее выполняется цикл до тех пор, пока функция getchar не вернет символ, совпадающий с первой буквой одного из элементов массива option.

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

$ ./menu1

Choice: Please select an action

a — add new record

d — delete record

q — quit

a

You have chosen: a

Choice: Please select an action

a — add new record

d — delete record

q — quit

Incorrect choice, select again

Choice: Please select an action

а — add new record

d — delete record

q — quit

q

You have chosen: q $

Для того чтобы сделать выбор, пользователь должен последовательно нажать клавиши <А>, <Enter>, <Q>, <Enter>. Здесь возникают, как минимум, две проблемы; самая серьезная заключается в том, что вы получаете сообщение "Incorrect choice" ("Неверный выбор") после каждого корректного выбора. Кроме того, вы еще должны нажать клавишу <Enter> (или <Return>), прежде чем программа считает введенные данные.

Сравнение канонического и неканонического режимов

Обе эти проблемы тесно связаны. По умолчанию ввод терминала не доступен программе до тех пор, пока пользователь не нажмет клавишу <Enter> или <Return>. В большинстве случаев это достоинство, поскольку данный способ позволяет пользователю корректировать ошибки набора с помощью клавиш <Backspace> или <Delete>. Только когда он остается доволен увиденным на экране, пользователь нажимает клавишу <Enter>, чтобы ввод стал доступен программе.

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

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

Помимо всего прочего, обработчик терминала в ОС Linux помогает превращать символы прерываний в сигналы (например, останавливающие выполнение программы, когда вы нажмете комбинацию клавиш <Ctrl>+<C>), он также может автоматически выполнить обработку нажатых клавиш <Backspace> и <Delete> и вам не придется реализовывать ее в каждой написанной вами программе. О сигналах вы узнаете больше в главе 11.

Итак, что же происходит в данной программе? ОС Linux сохраняет ввод до тех пор, пока пользователь не нажмет клавишу <Enter>, и затем передает в программу символ выбранного пункта меню и следом за ним код клавиши <Enter>. Каждый раз, когда вы вводите символ пункта меню, программа вызывает функцию getchar, обрабатывает символ и снова вызывает getchar, немедленно возвращающую символ клавиши <Enter>.

Символ, который на самом деле видит программа, — это не символ ASCII возврата каретки CR (десятичный код 13, шестнадцатеричный 0D), а символ перевода строки LF (десятичный код 10, шестнадцатеричный 0A). Так происходит потому, что на внутреннем уровне ОС Linux (как и UNIX) всегда применяет перевод строки для завершения текстовых строк, т. е. в отличие от других ОС, таких как MS-DOS, использующих комбинацию символов возврата каретки и перевода строки, ОС UNIX применяет, для обозначения новой строки только символ перевода строки. Если вводное или выводное устройство посылает или запрашивает и символ возврата каретки, в ОС Linux об этом заботится обработчик терминала. Если вы привыкли работать в MS-DOS или других системах, это может показаться странным, но одно из существенных преимуществ заключается в отсутствии в ОС Linux реальной разницы между текстовыми и бинарными файлами. Символы возврата каретки обрабатываются, только когда вы вводите или выводите их на терминал или некоторые принтеры и плоттеры.

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

do {

 selected = getchar();

} while (selected == '\n');

Он решает непосредственно возникшую проблему, и вы увидите вывод, подобный приведенному далее:

$ ./menu1

Choice: Please select an action

a — add new record

d — delete record

q — quit

a

You have chosen: a

Choice: Please select an action

a — add new record

d — delete record

q — quit

q

You have chosen: q $

Мы вернемся позже ко второй проблеме, связанной с необходимостью нажимать клавишу <Enter>, и более элегантному решению для обработки символа перевода строки.

Обработка перенаправленного вывода

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

$ ./menu1 > file

a

q

$

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

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

#include <unistd.h>

int isatty(int fd);

Системный вызов isatty возвращает 1, если открытый дескриптор файла fd связан с терминалом, и 0 в противном случае.

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

Что вы собираетесь делать, если стандартный вывод stdout перенаправлен? Просто завершить программу — не слишком хорошо, потому что у пользователя нет возможности выяснить, почему программа аварийно завершила выполнение. Вывод сообщения в stdout тоже не поможет, поскольку оно будет перенаправлено с терминала. Единственное решение — записать сообщение в стандартный поток ошибок stderr, который не перенаправляется командой оболочки > file (упражнение 5.2).

Упражнение 5.2. Проверка для выявления перенаправления вывода

Внесите следующие изменения в директивы включения заголовочных файлов и функцию main программы menu1.с из упражнения 5.1. Назовите новый файл menu2.c.

#include <unistd.h>

...

int main() {

 int choice = 0;

 if (!isatty(fileno(stdout))) {

  fprintf(stderr, "You are not a terminal!\n");

  exit(1);

 }

 do {

  choice = getchoice("Please select an action", menu);

  printf("You have chosen: %c\n", choice);

 } while (choice != 'q');

 exit(0);

}

Теперь посмотрите на следующий пример вывода:

$ ./menu2

Choice: Please select an action

a — add new record

d — delete record

q — quit

q

You have chosen: q $ ./menu2 > file

You are not a terminal! $

Как это работает

В новом фрагменте программного кода функция isatty применяется для проверки связи стандартного вывода с терминалом и прекращения выполнения программы при отсутствии этой связи. Это тот же самый тест, который командная оболочка использует для решения, нужно ли выводить строки приглашения. Возможно и довольно обычно перенаправление и stdout, и stderr с терминала на другое устройство. Вы можете направить поток ошибок в другой файл:

$ ./menu2 >file 2>file.error

$

или объединить оба выводных потока в одном файле:

$ ./menu2 >file 2>&1

$

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

Диалог с терминалом

Если нужно защитить части вашей программы, взаимодействующие с пользователем, от перенаправления, но разрешить его для других входных и выходных данных, вы должны отделить общение с пользователем от потоков stdout и stderr. Это можно сделать, непосредственно считывая данные с терминала и прямо записывая данные на терминал. Поскольку ОС Linux с самого начала создавалась, как многопользовательская система, включающая, как правило, множество терминалов, как непосредственно подсоединенных, так и подключенных по сети, как вы сможете определить тот терминал, который следует использовать?

К счастью, Linux и UNIX облегчают жизнь, предоставляя специальное устройство /dev/tty, которое всегда является текущим терминалом или сеансом работы в системе (login session). Поскольку ОС Linux все интерпретирует как файлы, вы можете выполнять обычные файловые операции для чтения с устройства /dev/tty и записи на него.

В упражнении 5.3 вы исправите программу выбора пункта меню так, чтобы можно было передавать параметры в подпрограмму getchoice и благодаря этому лучше управлять выводом. Назовите ее menu3.c.

Упражнение 5.3. Применение /dev/tty

Загрузите файл menu2.c и измените программный код так, чтобы входные и выходные данные приходили с устройства /dev/tty и направлялись на это устройство.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

char *menu[] = {

 "a — add new record", "d — delete record", "q - quit", NULL,

};

int getchoice(char* greet, char* choices[], FILE* in, FILE* out);

int main() {

 int choice = 0;

 FILE* input;

 FILE* output;

 if (!isatty(fileno(stdout))) {

  fprintf(stderr, "You are not a terminal, OK.\n");

 }

 input = fopen("/dev/tty", "r");

 output = fopen("/dev/tty", "w");

 if (!input || !output) {

  fprintf(stderr, "Unable to open /dev/tty\n");

  exit(1);

 }

 do {

  choice = getchoice("Please select an action", menu, input, output);

  printf("You have chosen: %c\n", choice);

 } while (choice != 'q');

 exit(0);

}

int getchoice(char* greet, char *choices[], FILE* in, FILE *out) {

 int chosen = 0;

 int selected;

 char **option;

 do {

  fprintf(out, "Choice: %s\n", greet);

  option = choices;

  while (*option) {

   fprintf(out, "%s\n", *option);

   option++;

  }

  do {

   selected = fgetc(in);

  } while(selected == '\n');

  option = choices;

  while (*option) {

   if (selected == *option[0]) {

    chosen = 1;

    break;

   }

   option++;

  }

  if (!chosen) {

   fprintf(out, "Incorrect choice, select again\n");

  }

 } while (!chosen);

 return selected;

}

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

$ ./menu3 > file

You are not a terminal, OK.

Choice: Please select an action

a — add new record

d — delete record

q — quit

d

Choice: Please select an action

a — add new record

d - delete record

q — quit

q

$ cat file

You have chosen: d

You have chosen: q

Драйвер терминала A и общий терминальный интерфейс

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

Обзор

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

Рис. 5.1 

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

К основным функциям, которыми вы можете управлять, относятся следующие:

□ редактирование строки — применение для редактирования клавиши <Backspace>;

□ буферизация — считывание символов сразу или после настраиваемой задержки;

□ отображение — управление отображением так же, как при считывании паролей;

□ CR/LF — отображение для ввода и вывода: что происходит при выводе символа перевода строки (\n);

□ скорости передачи данных по линии — редко применяется для консоли ПК, эти скорости очень важны для модемов и терминалов на линиях последовательной передачи.

Аппаратная модель

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

Концептуальная схема (физическая модель на некоторых старых узлах UNIX подобна данной) включает машину с ОС UNIX, подключенную через последовательный порт с модемом и далее по телефонной линии с другим модемом к удаленному терминалу (рис. 5.2). На деле это просто вариант установки, применявшийся некоторыми малыми провайдерами интернет-услуг "на заре туманной юности" Интернета. Эта модель отдаленно напоминает организацию "клиент — сервер", при использовании которой программа выполняется на большом компьютере, а пользователи работают на терминалах ввода/вывода.

Рис. 5.2

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

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

Структура типа termios

Тип termios — стандартный интерфейс, заданный стандартом POSIX и похожий на интерфейс termio системы System V. Интерфейс терминала управляется значениями в структуре типа termios и использует небольшой набор вызовов функций. И то и другое определено в заголовочном файле termios.h.

Примечание

Программы, применяющие вызовы функций, определенных в файле termios.h, нуждаются в компоновке с соответствующей библиотекой функций. Ею может быть в зависимости от установленной у вас системы просто стандартная библиотека С или библиотека curses. При необходимости во время компиляции примеров этой главы добавьте аргумент -lcurses в конец строки команды компиляции. В некоторых более старых системах Linux библиотека curses представлена в версии, известной под названием "new curses". В этих случаях имя библиотеки и аргумент компоновки становятся ncurses и -lncurses соответственно.

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

□ ввод;

□ вывод;

□ управление;

□ локальный;

□ специальные управляющие символы.

Минимальная структура типа termios обычно объявляется следующим образом (хотя в стандарте X/Open разрешено включение дополнительных полей):

#include <termios.h>

struct termios {

 tcflag_t c_iflag;

 tcflag_t c_oflag;

 tcflag_t c_cflag;

 tcflag_t c_lflag;

 cc_t c_cc[NCCS];

};

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

Инициализировать структуру типа termios для терминала можно, вызвав функцию tcgetattr со следующим прототипом или описанием:

#include <termios.h>

int tcgetattr(int fd, struct termios *termios_p);

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

#include <termios.h>

int tcsetattr(int fd, int actions, const struct termios *termios_p);

Поле actions функции tcsetattr управляет способом внесения изменений. Есть три варианта:

TCSANOW — изменяет значения сразу;

TSCADRAIN — изменяет значения, когда текущий вывод завершен;

TCSAFLUSH — изменяет значения, когда текущий вывод завершен, но отбрасывает любой ввод, доступный в текущий момент и все еще не возвращенный вызовом read.

Примечание

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

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

Наиболее важный режим, который следует принять во внимание при первом прочтении, — локальный (local). Канонический и неканонический режимы — решение второй проблемы в вашем первом приложении: пользователь должен нажимать клавишу <Enter> или <Return> для чтения программой входных данных. Вам следует заставить программу ждать всю строку ввода или набрасываться на ввод, как только он набран на клавиатуре.

Режимы ввода

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

В элементе c_iflag могут применяться следующие макросы:

□ BRKINT — генерирует прерывание, когда в линии связи обнаруживается разрыв (потеря соединения);

□ IGNBRK — игнорирует разрывы соединения в линии связи;

□ ICRNL — преобразует полученный символ возврата каретки в символ перехода на новую строку;

□ IGNCR — игнорирует полученные символы возврата каретки;

□ INLCR — преобразует полученные символы перехода на новую строку в символы возврата каретки;

□ IGNPAR — игнорирует символы с ошибками четности;

□ INCPK — выполняет контроль четности у полученных символов;

□ PARMRK — помечает ошибки четности;

□ ISTRIP — обрезает (до семи битов) все входные символы;

□ IXOFF — включает программное управление потоком при вводе;

□ IXON — включает программное управление потоком при выводе.

Примечание

Если флаги BRKINT и IGNBRK не установлены, сбой на линии связи считывается как символ NULL (0x00).

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

Режимы вывода

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

Вы управляете режимами вывода, устанавливая флаги элемента c_oflag структуры типа termios. В элементе c_oflag могут применяться следующие макросы:

□ OPOST — включает обработку вывода;

□ ONLCR — преобразует в символ перевода строки пару символов возврат каретки/перевод строки;

□ OCRNL — преобразует любой символ возврата каретки в выводе в символ перевода строки;

□ ONOCR — не выводит символ возврата каретки в столбце 0;

□ ONLRET — символ перехода на новую строку выполняет возврат каретки;

□ OFILL — посылает символы заполнения для формирования задержки;

□ OFDEL — применяет символ DEL как заполнитель вместо символа NULL;

□ NLDLY — выбор задержки для символа перехода на новую строку;

□ CRDLY — выбор задержки для символа возврата каретки;

□ TABDLY — выбор задержки для символа табуляции;

□ BSDLY — выбор задержки для символа Backspace;

□ VTDLY — выбор задержки для символа вертикальной табуляции;

□ FFDLY — выбор задержки для символа прокрутки страницы.

Примечание

Если флаг OPOST не установлен, все остальные флаги игнорируются.

Режимы вывода тоже обычно не используются, поэтому мы не будем их обсуждать в дальнейшем.

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

Эти режимы управляют аппаратными характеристиками терминала. Вы задаете режимы управления, устанавливая флаги элемента c_cflag структуры типа termios, включающие следующие макросы:

□ CLOCAL — игнорирует управление линиями с помощью модема;

□ CREAD — включает прием символов;

□ CS5 — использует пять битов в отправляемых и принимаемых символах;

□ CS6 — использует шесть битов в отправляемых и принимаемых символах;

□ CS7 — использует семь битов в отправляемых и принимаемых символах;

□ CS8 — использует восемь битов в отправляемых и принимаемых символах;

□ CSTOPB — устанавливает два стоповых бита вместо одного;

□ HUPCL — выключает управление линиями модема при закрытии;

□ PARENB — включает генерацию и проверку четности;

□ PARODD — применяет контроль нечетности вместо контроля четности.

Примечание

Если драйвер терминала обнаруживает, что последний дескриптор файла, ссылающийся на терминал, закрыт и при этом флаг HUPCL установлен, он устанавливает линии управления модема в состояние останова (hang-up).

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

Локальные режимы

Эти режимы управляют разнообразными характеристиками терминала. Вы можете задать локальный режим, устанавливая флаги элемента c_iflag структуры termios с помощью следующих макросов:

□ ECHO — включает локальное отображение вводимых символов;

□ ECHOE — выполняет комбинацию Backspace, Space, Backspace при получении символа ERASE (стереть);

□ ECHOK — стирает строку при получении символа KILL;

□ ECHONL — отображает символы перехода на новую строку;

□ ICANON — включает стандартную обработку ввода (см. текст, следующий за данным перечнем);

□ IEXTEN — включает функции, зависящие от реализации;

□ ISIG — включает генерацию сигналов;

□ NOFLSH — отключает немедленную запись очередей;

□ TOSTOP — посылает сигнал фоновым процессам при попытке записи.

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

Специальные управляющие символы

Специальные управляющие символы — это коллекция символов подобных символам от комбинации клавиш <Ctrl>+<C>, действующих особым образом, когда пользователь вводит их. В элементе c_cc структуры termios содержатся символы, отображенные на поддерживаемые функции. Позиция каждого символа (его номер в массиве) определяется макросом, других ограничений для управляющих символов не задано.

Массив c_cc используется двумя очень разными способами, зависящими от того, установлен для терминала канонический режим (т.е. установлен флаг ICANON в элементе c_lflag структуры termios) или нет.

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

Для канонического режима применяются следующие индексы:

□ VEOF — символ EOF;

□ VEOL — дополнительный символ конца строки EOL;

□ VERASE — символ ERASE;

□ VINTR — символ прерывания INTR;

□ VKILL — символ уничтожения KILL;

□ VQUIT — символ завершения QUIT;

□ VSUSP — символ приостанова SUSP;

□ VSTART — символ запуска START;

□ VSTOP — символ останова STOP.

Для канонического режима применяются следующие индексы:

□ VINTR — символ INTR;

□ VMIN — минимальное значение MIN;

□ VQUIT — символ QUIT;

□ VSUSP — символ SUSP;

□ VTIME — время ожидания TIME;

□ VSTART — символ START;

□ VSTOP — символ STOP.

Символы

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

Таблица 5.1

Символ Описание
INTR Заставляет драйвер терминала отправить сигнал SIGINT процессам, подключенным к терминалу. Мы обсудим сигналы более подробно в главе 11
QUIT Заставляет драйвер терминала отправить сигнал SIGQUIT процессам, подключенным к терминалу
ERASE Заставляет драйвер терминала удалить последний символ в строке
KILL Заставляет драйвер терминала удалить всю строку
EOF Заставляет драйвер терминала передать все символы строки во ввод, считываемый приложением. Если строка пустая, вызов read вернет ноль символов, как будто он встретил на конец файла
EOL Действует как ограничитель строки в дополнение к более привычному символу перехода на новую строку
SUSP Заставляет драйвер терминала послать сигнал SIGSUSP процессам, подключенным к терминалу. Если ваша система UNIX поддерживает управление заданиями, текущее приложение будет приостановлено
STOP Действует как "прерыватель потока", т. е. прекращает дальнейший вывод на терминал. Применяется для поддержки управления потоком XON/XOFF и обычно задается как ASCII-символ XOFF (<Ctrl>+<S>)
START Возобновляет вывод после символа STOP, часто ASCII-символ XON

Значения TIME и MIN

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

Возможны четыре варианта.

□ MIN = 0 и TIME = 0. В этом случае вызов read всегда завершается сразу же. Если какие-то символы доступны, они будут возвращены, если нет, то read вернет ноль, и никакие символы не будут считаны.

□ MIN = 0 и TIME > 0. В этом случае вызов read завершится, когда все доступные символы будут считаны или когда пройдет TIME десятых долей секунды. Если нет прочитанных символов из-за превышения отпущенного времени, read вернет 0. В противном случае он вернет количество прочитанных символов.

□ MIN > 0 и TIME = 0. В этом случае вызов read будет ждать до тех пор, пока можно будет считать MIN символов, и затем вернет это количество символов. В случае конца файла возвращается 0.

□ MIN > 0 и TIME > 0. Это самый сложный случай. После вызова read ждет получения символа. Когда первый символ получен, каждый раз при получении последующего символа запускается межсимвольный таймер (или перезапускается, если он уже был запущен). Вызов read завершится, когда либо можно будет считать MIN символов, либо межсимвольное время превысит TIME десятых долей секунды. Это может пригодиться для подсчета разницы между единственным нажатием клавиши <Esc> и запуском функциональной клавиатурной escape-последовательности. Тем не менее следует знать, что сетевые соединения или высокая загрузка процессора могут полностью стереть такие полезные сведения о времени.

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

Доступ к режимам терминала из командной оболочки

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

$ stty -a

На установленных у авторов системах Linux, обладающих структурами termios с некоторыми расширениями по сравнению со стандартными, получен следующий вывод:

speed 38400 baud; rows 24; columns 80; line = 0;

intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;

eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;

werase = ^W; lnext = ^V; flush = ^O, min = 1; time = 0;

-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts

-ignbrk -brkint -ignpar -parmirk -inpck -istrip -inlcr -igncr icrnl -ixon -ixoff

-iuclc -ixany -imaxbe1 iutf8

opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel n10 cr0 tab0 bs0 vt0 ff0

isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt

echoctl echoke

Среди прочего, как видите, символ EOF — это <Ctrl>+<D>, и включено отображение. Экспериментируя с установками терминала, легко получить в результате терминал в нестандартном режиме, что затруднит его дальнейшее использование. Есть несколько способов справиться с этой трудностью.

□ Первый способ — применить следующую команду, если ваша версия stty поддерживает ее:

$ stty sane

Если вы потеряли преобразование клавиши возврата каретки в символ перехода на новую строку (который завершает строку), возможно, потребуется ввести stty sane, но вместо нажатия клавиши <Enter> нажать комбинацию клавиш <Ctrl>+<J> (которая обозначает переход на новую строку).

□ Второй способ — применить команду stty -g и записать текущие установки stty в форму, готовую к повторному считыванию. В командной строке вы можете набрать следующее:

$ stty -g > save_stty

...

<эксперименты с параметрами>

...

$ stty $(cat save_stty)

В финальной команде stty вам все еще придется использовать комбинацию клавиш <Ctrl>+<J> вместо клавиши <Enter>. Ту же самую методику можно применить и в сценариях командной оболочки.

save_stty="$(stty -g)"

<изменение stty-параметров>

stty $save_stty

□ Если вы все еще в тупике, третий способ — перейти на другой терминал, применить команду ps для поиска оболочки, которую вы сделали непригодной, и затем использовать команду kill hup <id процесса> для принудительного завершения этой командной оболочки. Поскольку перед выводом регистрационного приглашения параметры stty всегда восстанавливаются, у вас появится возможность нормально зарегистрироваться в системе еще раз.

Задание режимов терминала из командной строки

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

Для установки режима, в котором ваш сценарий командной оболочки сможет выполнять посимвольное считывание, вы должны отключить канонический режим и задать 1 и 0. Команда будет выглядеть следующим образом:

$ stty -icanon min 1 time 0

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

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

$ stty -echo

Примечание

Не забудьте применить команду stty echo для возврата отображения после ваших экспериментов!

Скорость терминала

Последняя функция, обслуживаемая структурой termios, — манипулирование скоростью линии передачи. Для этой скорости не определен отдельный элемент структуры; она управляется вызовами функций. Скорости ввода и вывода обрабатываются отдельно.

Далее приведены четыре прототипа вызовов:

#include <termios.h> 

speed_t cfgetispeed(const struct termios *);

speed_t cfgetospeed(const struct termios *);

int cfsetispeed(struct termios *, speed_t speed);

int cfsetospeed(struct termios *, speed_t speed);

Обратите внимание на то, что они воздействуют на структуру termios, а не непосредственно на порт. Это означает, что для установки новой скорости вы должны считать текущие установки с помощью функции tcgetattr, задать скорость, применив приведенные вызовы, и затем записать структуру termios обратно с помощью функции tcsetattr. Скорость линии передачи изменится только после вызова tcsetattr.

В вызовах перечисленных функций допускается задание разных значений скорости speed, но к основным относятся следующие константы:

□ B0 — отключение терминала;

□ B1200 — 1200 бод;

□ B2400— 2400 бод;

□ B9600 — 9600 бод;

□ B19200 — 19 200 бод;

□ B38400 — 38 400 бод.

Не существует скоростей выше 38 400 бод, задаваемых стандартом, и стандартного метода обслуживания последовательных портов на более высоких скоростях.

Примечание

В некоторых системах, включая Linux, для выбора более высоких скоростей определены константы В57600, B115200 и В230400. Если вы пользуетесь более старой версией ОС Linux и эти константы недоступны, можно применить команду setserial для получения нестандартных скоростей 57 600 и 115 200. В этом случае указанные скорости будут использоваться при выборе константы B38400. Оба эти метода непереносимы, поэтому применяйте их с осторожностью.

Дополнительные функции

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

#include <termios.h>

int tcdrain(int fd);

int tcflow(int fd, int flowtype);

int tcflush(int fd, int in_out_selector);

Функции предназначены для следующих целей:

□ tcdrain — заставляет вызвавшую программу ждать до тех пор, пока не будет отправлен весь поставленный в очередь вывод;

□ tcflow — применяется для приостановки или возобновления вывода;

□ tcflush — может применяться для отказа от входных или выходных данных либо и тех, и других.

Теперь, когда мы уделили довольно много внимания структуре termios, давайте рассмотрим несколько практических примеров. Возможно, самый простой из них — отключение отображения при чтении пароля (упражнение 5.4). Это делается сбрасыванием флага echo.

Упражнение 5.4. Программа ввода пароля с применение termios

1. Начните вашу программу password.с со следующих определений:

#include <termios.h>

#include <stdio.h>

#include <stdlib.h>

#define PASSWORD_LEN 8

int main() {

 struct termios initialrsettings, newrsettings;

 char password[PASSWORD_LEN + 1];

2. Далее добавьте строку, считывающую текущие установки из стандартного ввода и копирующую их в только что созданную вами структуру типа termios:

 tcgetattr(fileno(stdin), &initialrsettings);

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

 newrsettings = initialrsettings;

 newrsettings.с_lflag &= ~ЕСНО;

 printf("Enter password: ");

4. Далее установите атрибуты терминала в newrsettings и считайте пароль. И наконец, восстановите первоначальные значения атрибутов терминала и выведите пароль на экран, чтобы свести на нет все предыдущие усилия по обеспечению безопасности:

 if (tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) {

  fprintf(stderr, "Could not set attributes\n");

 } else {

  fgets(password, PASSWORD_LEN, stdin);

  tcsetattr(fileno(stdin), TCSANOW, &initialrsettings);

  fprintf(stdout, "\nYou entered %s\n", password);

 }

 exit(0);

}

Когда вы выполните программу, то увидите следующее:

$ ./password

Enter password: You entered hello

$ 

Как это работает

В этом примере слово hello набирается на клавиатуре, но не отображается на экране в строке приглашения Enter password:. Никакого вывода нет до тех пор, пока пользователь не нажмет клавишу <Enter>.

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

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

Другой распространенный пример использования структуры termios — перевод терминала в состояние, позволяющее вам считывать каждый набранный символ (упражнение 5.5). Для этого отключается канонический режим и используются параметры MIN и TIME.

Упражнение 5.5. Считывание каждого символа

Применяя только что полученные знания, вы можете изменить программу menu. Приведенная далее программа menu4.c базируется на программе menu3.c и использует большую часть кода из файла password.с, включенного в нее. Внесенные изменения выделены цветом и объясняются в пунктах описания.

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

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <termios.h>

char *menu[] = {

 "a — add new record",

 "d — delete record",

 "q - quit",

 NULL,

};

2. Затем нужно объявить пару новых переменных в функции main:

int getchoice(char *greet, char *choices[], FILE *in, FILE *out);

int main() {

 int choice = 0;

 FILE *input;

 FILE *output;

 struct termios initial_settengs, new_settings;

3. Перед вызовом функции getchoice вам следует изменить характеристики терминала, этим определяется место следующих строк:

 if (!isatty(fileno(stdout))) {

  fprintf(stderr, "You are not a terminal, OK.\n");

 }

 input = fopen("/dev/tty", "r");

 output = fopen("/dev/tty", "w");

 if (!input || !output) {

  fprintf(stderr, "Unable to open /dev/tty\n");

  exit(1);

 }

 tcgetattr(fileno(input), &initial_settings);

 new_settings = initial_settings;

 new_settings.c_lfag &= ~ICANON;

 new_settings.c_lflag &= ~ECHO;

 new_settings.c_cc[VMIN] = 1;

 new_settings.c_cc[VTIME] = 0;

 new_settings.c_lflag &= ~ISIG;

 if (tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {

  fprintf(stderr, "could not set attributes\n");

 }

4. Перед завершением вы также должны вернуть первоначальные значения:

 do {

  choice = getchoice("Please select an action", menu, input, output);

  printf("You have chosen: %c\n", choice);

 } while (choice != 'q');

 tcsetattr(fileno(input), TCSANOW, &initial_settings);

 exit(0);

}

5. Теперь, когда вы в неканоническом режиме, необходимо проверить на соответствие возвраты каретки, поскольку стандартное преобразование CR (возврат каретки) в LF (переход на новую строку) больше не выполняется:

int getchoice (char *greet, char *choices[], FILE *in, FILE *out) {

 int chosen = 0;

 int selected;

 char **option;

 do {

  fprintf(out, "Choice: %s\n", greet);

  option = choices;

  while (*option) {

   fprintf(but, "%s\n", *option);

   option++;

  }

  do {

   selected = fgetc(in);

  } while (selected == '\n' || selected == '\r');

  option = choices;

  while (*option) {

   if (selected == *option[0]) {

    chosen = 1;

    break;

   }

   option++;

  }

  if (!chosen) {

   fprintf(out, "Incorrect choice, select again\n");

  }

 } while(!chosen);

 return selected;

}

Пока вы не устроите все иначе, теперь, если пользователь нажмет в вашей программе комбинацию клавиш <Ctrl>+<C>, программа завершится. Вы можете отключить обработку этих специальных символов, очистив флаг ISIG в локальных режимах. Для этого в функцию main включается следующая строка:

new_settings.c_lflag &= ~ISIG;

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

$ ./menu4

Choice: Please select an action

a — add new record

d — delete record

q — quit

You have chosen: a

Choice: Please select an action

a — add new record

d — delete record

q — quit

You have chosen: q $

Если вы нажмете комбинацию клавиш <Ctrl>+<C>, символ будет передан прямо в программу и будет истолкован, как неверный выбор.

Вывод терминала

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

Тип терминала

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

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

Примечание

Существует стандарт ANSI для набора escape-последовательностей (в основном базирующихся на последовательностях, применяемых в серии VT-терминалов компании Digital Equipment Corporation, но не идентичных им). Многие терминальные программы обеспечивают эмуляцию стандартного аппаратного терминала, часто VT100, VT220 или ANSI, а иногда и других типов.

Такое разнообразие аппаратных моделей терминалов было бы огромной проблемой для программистов, пытающихся написать программы управления экраном, выполняющиеся на терминалах разных типов. Например, терминал ANSI применяет последовательность символов Escape, [, A для перемещения курсора вверх на одну строку. Терминал ADM-За (очень распространенный несколько лет назад) использует один управляющий символ от комбинации клавиш <Ctrl>+<K>.

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

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

Для применения функций terminfo вы, как правило, должны подключить заголовочный файл curses.h пакета curses и собственный заголовочный файл term.h пакета terminfo. В некоторых системах Linux вам, возможно, придется применять реализацию curses, известную как ncurses, и включить файл ncurses.h для предоставления прототипов вашим функциям terminfo.

Установите тип вашего терминала

Окружение ОС Linux содержит переменную TERM, которая хранит тип используемого терминала. Обычно она устанавливается системой автоматически во время регистрации в системе. Системный администратор может задать тип терминала по умолчанию для каждого непосредственно подключенного терминала и может сформировать подсказку с типом терминала для удаленных сетевых пользователей. Значение TERM может быть передано rlogin через telnet.

Пользователь может запросить командную оболочку о соображениях системы по поводу используемого им или ею терминала:

$ echo $TERM

xterm

$

В данном случае оболочка выполняется из программы, называемой xterm — эмулятора терминала для графической оболочки X Window System, или программы, обеспечивающей "такие же функциональные возможности, как KDE's Konsole или GNOME's gnome-terminal.

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

Характеристики терминалов в terminfo описываются с помощью атрибутов. Они хранятся в наборе откомпилированных файлов terminfo, которые обычно находятся в каталогах /usr/lib/terminfo или /usr/share/terminfo. Для каждого терминала (и многих принтеров, которые тоже могут быть заданы в terminfo) есть файл, в котором определены характеристики терминала и способ доступа к его функциям. Для того чтобы не создавать слишком большого каталога, реальные файлы хранятся в подкаталогах, имена которых — первый символ типа терминала. Так определение терминала VT100 можно найти в файле …terminfo/v/vt100.

Файлы terminfo пишутся по одному на каждый тип терминала в исходном формате, пригодном (или почти пригодном!) для чтения, который затем компилируется командой tic в более компактный и эффективный формат, используемый прикладными программами. Странно, стандарт X/Open ссылается на описания исходного и откомпилированного формата, но не упоминает команду tic, необходимую для реального преобразования исходного формата в откомпилированный. Для вывода пригодной для чтения версии откомпилированного элемента набора terminfo можно использовать программу infocmp.

Далее приведен пример файла terminfo для терминала VT100:

$ infocmp vt100

vt100|vt100-am|dec vt100 (w/advanced video),

 am, mir, msgr, xenl, xon, cols#80, it#8, lines#24, vt#3,

 acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,

 bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>,

 clear=\E[H\E[J$<50>, cr=\r, csr=\E[%i%p1%d;%p2%dr,

 cub=\E[%p1%dD, cub1=\b, cud=\E[%p1%dB, cud1=\n,

 cuf=\E[%p1%dC, cuf1=\E[C$<2>,

 cup=\E[%i%p1%d; %p2%dH$<5>, cuu=\E[%p1%dA,

 cuu1=\E[A$<2>, ed=\E[J$<50>, el=\E[K$<3>,

 el1=\E[1K$<3>, enacs=\E(B\E)0, home=\E[H, ht=\t,

 hts=\EH, ind=\n, ka1=\EOq, ka3=\EOs, kb2=\EOr, kbs=\b,

 kc1=\EOp, kc3=\EOn, kcub1=\EOD, kcud1=\EOB,

 kcuf1=\EOC, kcuu1=\EOA, kent=\EOM, kf0=\EOy, kf1=\EOP,

 kf10=\EOx, kf2=\EOQ, kf3=\EOR, kf4=\EOS, kf5=\EOt,

 kf6=\EOu, kf7=\EOv, kf8=\EOl, kf9=\EOw, rc=\E8,

 rev=\E[7m$<2>, ri=\EM$<5>, rmacs=^O, rmkx=\E[?11\E>,

 rmso=\E[m$<2>, rmul=\E[m$<2>,

 rs2=\E>\E[?31\E[?41\E[?51\E[?7h\E[?8h, sc=\E7,

 sgr=\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t^N%e^O%;,

 sgr0=\E[m^0$<2>, smacs=^N, smkx=\E[?1h\E=,

 smso=\E[1;7m$<2>; smul=\E[4m$<2>, tbc=\E[3g,

Каждое определение в terminfo состоит из трех типов элементов. Каждый элемент называется capname (имя характеристики) и определяет характеристику терминала.

Булевы или логические характеристики просто обозначают наличие или отсутствие поддержки терминалом конкретного свойства. Например, булева характеристика xon присутствует, если терминал поддерживает управление потоком XON/XOFF.

Числовые характеристики определяют размеры или объемы, например lines — это количество строк на экране, a cols — количество столбцов. Число отделяется от имени характеристики символом #. Для описания терминала с 80 столбцами и 24 строками следует написать cols#80, lines#24.

Строковые характеристики немного сложнее. Они применяются для двух разных типов характеристик: определения строк вывода, необходимых для доступа к функциям терминала, и определения строк ввода, которые будут получены, когда пользователь нажмет определенные клавиши, обычно функциональные или специальные клавиши на цифровой клавиатуре. Некоторые строковые параметры очень просты, например el, что означает "стереть до конца строки". Для того чтобы сделать это на терминале VT100, потребуется escape-последовательность Esc, [, K. В исходном формате terminfo это записывается как еl=\Е[K.

Специальные клавиши определены аналогичным образом. Например, функциональная клавиша <F1> на терминале VT100 посылает последовательность Esc, O, P, которая определяется как kf1=\EOP.

Все несколько усложняется, если escape-последовательности требуются какие-либо параметры. Большинство терминалов могут перемещать курсор в заданные строку и столбец. Ясно, что неразумно хранить отдельную характеристику для каждой точки экрана, в которую можно переместить курсор, поэтому применяется общая строковая характеристика с параметрами, определяющими значения, которые вставляются при использовании характеристики. Например, терминал VT100 использует последовательность Esc, [, <row>, <col>, H для перемещения курсора в заданную позицию. В исходном формате terminfo это записывается довольно устрашающе: cup=\E[%i%p1%d;%p2%dH$<5>.

Эта строка означает следующее:

□ \E — послать escape-символ;

□ [ — послать символ [;

□ %i — дать приращение аргументам;

□ %p1 — поместить первый аргумент в стек;

□ %d — вывести число из стека как десятичное;

□ ; — послать символ ;;

□ %р2 — поместить второй аргумент в стек;

□ %d — вывести число из стека как десятичное;

□ H —послать символ H.

Данная запись кажется сложной, но позволяет задавать параметры в строгом порядке, не зависящем от порядка, в котором терминал ожидает их появления в финальной escape-последовательности. Приращение аргументов %i необходимо, поскольку стандартная адресация курсора задается, начиная от верхнего левого угла экрана (0, 0), а терминал VT100 обозначает начальную позицию курсора как (1, 1). Заключительные символы $<5> означают, что для обработки терминалом перемещения курсора требуется задержка, эквивалентная времени вывода пяти символов.

Примечание

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

Применение характеристик terminfo

Теперь, когда вы знаете, как определить характеристики терминала, нужно научиться обращаться к ним. Когда используется terminfo, прежде всего вам нужно задать тип терминала, вызвав функцию setupterm. Она инициализирует структуру TERMINAL для текущего типа терминала. После этого вы сможете запрашивать характеристики терминала и применять его функциональные возможности. Делается это с помощью вызова setupterm, подобного приведенному далее:

#include <term.h>

int setupterm(char *term, int fd, int *errret);

Библиотечная функция setupterm задает текущий тип терминала в соответствии с заданным параметром term. Если term — пустой указатель, применяется переменная окружения TERM. Открытый дескриптор файла, предназначенный для записи на терминал, должен передаваться в параметре fd. Результат функции хранится в целой переменной, на которую указывает errret, если это не пустой указатель. Могут быть записаны следующие значения:

□ -1 — нет базы данных terminfo;

□ 0 — нет совпадающего элемента в базе данных terminfo;

□ 1 — успешное завершение.

Функция setupterm возвращает константу OK в случае успешного завершения и ERR в случае сбоя. Если на параметр errret установлен как пустой указатель, setupterm выведет диагностическое сообщение и завершит программу в случае своего аварийного завершения, как в следующем примере:

#include <stdio.h>

#include <term.h>

#include <curses.h>

#include <stdlib.h>

int main() {

 setupterm("unlisted", fileno(stdout), (int *)0);

 printf("Done.\n");

 exit(0);

}

Результат выполнения этой программы в вашей системе может не быть точной копией приведенного далее, но его смысл будет вполне понятен. "Done." не выводится, поскольку функция setupterm после своего аварийного завершения вызвала завершение программы:

$ cc -о badterm badterm.с -lncurses

$ ./badterm

'unlisted': unknown terminal type.

$

Обратите внимание на строку компиляции в примере: в этой системе Linux мы используем реализацию ncurses библиотеки curses со стандартным заголовочным файлом, находящимся в стандартном каталоге. В таких системах вы можете просто включить файл curses.h и задать -lncurses для библиотеки.

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

#include <term.h>

int tigetflag(char *capname);

int tigetnum(char *capname);

char *tigetstr(char *capname);

Функции tigetflag, tigetnum и tigetstr возвращают значения характеристик terminfo булева или логического, числового и строкового типов соответственно. В случае сбоя (например, характеристика не представлена) tigetflag вернет -1, tigetnum — -2, a tigetstr — (char*)-1.

Вы можете применять базу данных terminfo для определения размера экрана терминала, извлекая характеристики cols и lines с помощью следующей программы sizeterm.c:

#include <stdio.h>

#include <term.h>

#include <curses.h>

#include <stdlib.h>

int main() {

 int nrows, ncolumns;

 setupterm(NULL, fileno(stdout), (int *)0);

 nrows = tigetnum("lines");

 ncolumns = tigetnum("cols");

 printf("This terminal has %d columns and %d rows\n", ncolumns, nrows);

 exit(0);

}

$ echo $TERM

vt100

$ ./sizeterm

This terminal has 80 columns and 24 rows

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

$ echo $TERM

xterm

$ ./sizeterm

This terminal has 88 columns and 40 rows

$

Если применить функцию tigetstr для получения характеристики перемещения курсора (cup) терминала типа xterm, вы получите параметризованный ответ: \Е[%p1%d;%p2%dH.

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

Вы можете заменить параметры в характеристике реальными значениями с помощью функции tparm. До девяти параметров можно заменить значениями и получить в результате применяемую escape-последовательность символов.

#include <term.h>

char *tparm(char *cap, long p1, long p2, ..., long p9);

После формирования escape-последовательности с помощью tparm, ее нужно отправить на терминал. Для корректной обработки этой последовательности не следует пересылать строку на терминал с помощью функции printf. Вместо нее примените одну из специальных функций, обеспечивающих корректную обработку любых задержек, необходимых для завершения операции, выполняемой терминалом. К ним относятся следующие:

#include <term.h>

int putp(char *const str);

int tputs(char *const str, int affcnt, int (*putfunc)(int));

В случае успешного завершения функция putp вернет константу OK,в противном случае — ERR. Эта функция принимает управляющую строку терминала и посылает ее в стандартный вывод stdout.

Итак, для перемещения в строку 5 и столбец 30 на экране можно применить блок программного кода, подобный приведенному далее:

char *cursor;

char *esc_sequence;

cursor = tigetstr("cup");

esc_sequence = tparm(cursor, 5, 30);

putp(esc_sequence);

Функция tputs предназначена для ситуаций, в которых терминал не доступен через стандартный вывод stdout, и позволяет задать функцию, применяемую для вывода символов. Она возвращает результат заданной пользователем функции putfunc. Параметр affcnt предназначен для обозначения количества строк, подвергшихся изменению. Обычно он устанавливается равным 1. Функция, используемая для вывода строки, должна иметь те же параметры и возвращать тип значения как у функции putfunc. В действительности putp(string) эквивалентна вызову tputs (string, 1, putchar). В следующем примере вы увидите применение функции tputs, используемой с функцией вывода, определенной пользователем.

Имейте в виду, что в некоторых старых дистрибутивах Linux последний параметр функции tputs определен как int (*putfunc)(char), что заставит вас изменить определение функции char_to_terminal из упражнения 5.6.

Примечание

Если вы обратитесь к страницам интерактивного справочного руководства за информацией о функции tparm и характеристиках терминалов, то можете встретить функцию tgoto. Причина, по которой мы не используем эту функцию, хотя она, очевидно, предлагает более легкий способ перемещения курсора, заключается в том, что она не включена в стандарт X/Open (Single UNIX Specification Version 2) по данным издания 1997 г. Следовательно, мы не рекомендуем применять любую из этих функций в ваших новых программах.

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

Для того чтобы собрать всю полученную информацию вместе, напишем окончательную версию примера программы выбора пункта меню screenmenu.c, в которой вы "нарисуете" варианты пунктов меню на экране для того, чтобы пользователь выбрал нужный пункт (упражнение 5.6).

Упражнение 5.6. Полное управление терминалом

Вы можете переписать функцию getchoice из программы menu4.c для предоставления полного управления терминалом. В этом листинге функция main пропущена, потому что она не меняется. Другие отличия от программы menu4.c выделены цветом.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <termios.h>

#include <term.h>

#include <curses.h>

static FILE* output_stream = (FILE *)0;

char *menu[] = {

 "a — add new record",

 "d — delete record",

 "q - quit",

 NULL,

};

int getchoice(char *greet, char *choices[], FILE *in, FILE *out);

int char_to_terminal(int_char_to_write);

int main() {

 ...

}

int getchoice(char *greet, char* choices[], FILE[]* in, FILE* out) {

 int chosen = 0;

 int selected;

 int screenrow, screencol = 10;

 char **option;

 char* cursor, *clear;

 output_stream = out;

 setupterm(NULL, fileno(out), (int*)0);

 cursor = tigetstr("cup");

 clear = tigetstr("clear");

 screenrow =4;

 tputs(clear, 1, (int*)char_to_terminal);

 tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);

 fprintf(out, "Choice: %s", greet);

 screenrow += 2;

 option = choices;

 while (*option) {

  ftputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);

  fprintf(out, "%s", *option);

  screenrow++;

  option++

 }

 fprintf(out, "\n");

 do {

  fflush(out);

  selected = fgetc(in);

  option = choices;

  while (*option) {

   if (selected == *option[0]) {

    chosen = 1;

    break;

   }

   option++;

  }

  if (!chosen) {

   tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);

   fprintf(out, "Incorrect choice, select again\n");

  }

 } while (!chosen);

 tputs(clear, 1, char_to_terminal);

 return selected;

}

int char_to_terminal(int char_to_write) {

 if (output_stream) putc(char_to_write, output_stream);

 return 0;

}

Сохраните эту программу как menu5.с.

Как это работает

Переписанная функция getchoice выводит то же меню, что и в предыдущих примерах, но подпрограммы вывода изменены так, чтобы можно было воспользоваться характеристиками из базы данных terminfo. Если вы хотите видеть на экране сообщение "You have chosen:" дольше, чем одно мгновение перед очисткой экрана и подготовкой его к следующему выбору пункта меню, добавьте в функцию main вызов sleep:

do {

 choice = getchoice("Please select an action", menu, input, output);

 printf("\nYou have chosen: %c\n", choice);

 sleep(1);

} while (choice != 'q');

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

В завершение этой главы бегло рассмотрим пример определения нажатий клавиш.

Обнаружение нажатий клавиш

Пользователи, программировавшие в ОС MS-DOS, часто ищут в ОС Linux эквивалент функции kbhit, которая определяет, была ли нажата клавиша, без реального ее считывания. К сожалению, их поиски оказываются безуспешными, поскольку прямого аналога нет. Программисты в среде UNIX не ощущают этого отсутствия, т.к. UNIX запрограммирована так, что программы очень редко (если когда-либо вообще) озабочены ожиданием события. Поскольку это обычный способ применения kbhit, ее нехватка редко ощущается в системах UNIX и Linux.

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

Упражнение 5.7. Исключительно ваша собственная kbhit

1. Начните со стандартной заголовочной информации и пары структур для установки параметров терминала. peek_character применяется для проверки нажатия клавиши. Далее описываются функции, которые будут использоваться позже:

#include <stdio.h>

#include <stdlib.h>

#include <termios.h>

#include <term.h>

#include <curses.h>

#include <unistd.h>

static struct termios initial_settings, new_settings;

static int peek_character = -1;

void init_keyboard();

void close_keyboard();

int kbhit();

int readch();

2. Функция main вызывает функцию init_keyboard для настройки терминала, затем выполняет цикл один раз в секунду, каждый раз вызывая в нем функцию kbhit. Если нажата клавиша <q>, функция close_keyboard восстанавливает нормальный режим и программа завершается:

int main() {

 int ch = 0;

 init_keyboard();

 while (ch != 'q') {

  printf("looping\n");

  sleep(1);

  if (kbhit()) {

   ch = readch();

   printf("you hit %c\n", ch);

  }

 }

 close_keyboard();

 exit(0);

}

3. Функции init_keyboard и close_keyboard настраивают терминал в начале и конце программы:

void init_keyboard() {

 tcgetattr(0, &initial_settings);

 new_settings = initial_settings;

 new_settings.c_lflag &= ~ICANON;

 new_settings.c_lflag &= ~ECHO;

 new_settings.c_lflag &= ~ISIG;

 new_settings.c_cc[VMIN] = 1;

 new_settings.c_cc[VTIME] = 0;

 tcsetattr(0, TCSANOW, &new_settings);

}

void close_keyboard() {

 tcsetattr(0, TCSANOW, &initial_settings);

}

4. Теперь функция, проверяющая нажатие клавиши:

int kbhit() {

 char ch;

 int nread;

 if (peek_character != -1) return 1;

 new_settings.c_cc[VMIN] = 0;

 tcsetattr(0, TCSANOW, &new_settings);

 nread = read(0, sch, 1);

 newrsettings.c_cc[VMIN] = 1;

 tcsetattr(0, TCSANOW, &new_settings);

 if (nread == 1) {

  peek_character = ch;

  return 1;

 }

 return 0;

}

5. Нажатый символ считывается следующей функцией readch, которая затем восстанавливает значение -1 переменной peek_character для выполнения следующего цикла:

int readch() {

 char ch;

 if (peek_character != -1) {

  ch = peek_character;

  peek_character = -1;

  return ch;

 }

 read(0, &ch, 1);

 return ch;

}

Когда вы выполните программу (kbhit.c), то получите следующий вывод:

$ ./kbhit

looping

looping

looping

you hit h

looping

looping

looping

you hit d

looping

you hit q

$

Как это работает

Терминал настраивается в функции init_keyboard на считывание одного символа (MIN=1, TIME=0). Функция kbhit изменяет это поведение на проверку ввода и его немедленный возврат (MIN=0, TIME=0) и затем восстанавливает исходные установки перед завершением.

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

Виртуальные консоли

ОС Linux предоставляет средство, называемое виртуальными консолями. Экран, клавиатуру и мышь одного ПК может использовать ряд терминальных устройств, доступных на этом компьютере. Обычно установка ОС Linux рассчитана на использование от 8 до 12 виртуальных консолей. Виртуальные консоли становятся доступными благодаря символьным устройствам /dev/ttyN, где N — номер, начинающийся с 1.

Если вы регистрируетесь в вашей системе Linux в текстовом режиме, как только система активизируется, вам будет предложено регистрационное приглашение. Далее вы регистрируетесь с помощью имени пользователя и пароля. В этот момент используемое вами устройство — первая виртуальная консоль, терминальное устройство /dev/tty1.

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

$ who

neil tty1 Mar 8 18:27

$ ps -e

 PID TTY      TIME CMD

1092 tty1 00:00:00 login

1414 tty1 00:00:00 bash

1431 tty1 00:00:00 emacs

Из этого укороченного вывода видно, что пользователь neil зарегистрировался и запустил редактор Emacs на консоли ПК, устройстве /dev/tty1.

Обычно ОС Linux запускается с процессом getty, выполняющимся на первых шести виртуальных консолях, поэтому есть возможность зарегистрироваться шесть раз, используя одни и те же экран, клавиатуру и мышь. Увидеть эти процессы можно с помощью и команды ps:

$ ps -а

 PID TTY      TIME CMD

1092 tty1 00:00:00 login

1093 tty2 00:00:00 mingetty

1094 tty3 00:00:00 mingetty

1095 tty4 00:00:00 mingetty

1096 tty5 00:00:00 mingetty

1097 tty6 00:00:00 mingetty

В этом выводе представлен стандартный вариант программы getty для обслуживания консоли в системе SUSE, mingetty, выполняющийся на пяти следующих виртуальных консолях и ожидающий регистрации пользователя.

Переключаться между виртуальными консолями можно с помощью комбинации клавиш <Ctrl>+<Alt>+<FN>, где N — номер виртуальной консоли, на которую вы хотите переключиться. Таким образом, для того чтобы перейти на вторую виртуальную консоль, нажмите <Ctrl>+<Alt>+<F2>, и <Ctrl>+<Alt>+<F1>, чтобы вернуться на первую консоль. (При переключении из регистрации в текстовом режиме, а не графическом, также работает комбинация клавиш <Ctrl>+<FN>.)

Если в Linux запущена регистрация в графическом режиме, либо с помощью программы startx илн менеджера экранов xdm, на первой свободной консоли, обычно /dev/tty7, стартует графическая оболочка X Window System. Переключиться с нее на текстовую, консоль вы сможете с помощью комбинации клавиш <Ctrl>+<Alt>+<FN>, а вернуться с помощью <Ctrl>+<Alt>+<F7>.

В ОС Linux можно запустить более одного сеанса X. Если вы сделаете это, скажем, с помощью следующей команды

$ startx -- :1

Linux запустит сервер X на следующей свободной виртуальной консоли, в данном случае на /dev/tty8, и переключаться между ними вы сможете с помощью комбинаций клавиш <Ctrl>+<Alt>+<F8> и <Ctrl>+<Alt>+<F7>.

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

Псевдотерминалы

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

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

Одно время реализация псевдотерминалов (если вообще существовала) сильно зависела от конкретной системы. Сейчас они включены в стандарт Single UNIX Specification (единый стандарт UNIX) как UNIX98 Pseudo-Terminals (псевдотерминалы стандарта UNIX98) или PTY.

Резюме 

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

Глава 6

Управление текстовыми экранами с помощью библиотеки curses

В главе 5 вы узнали, как улучшить управление вводом символов и как обеспечить вывод символов способом, не зависящим от особенностей конкретного терминала. Проблема использования общего терминального интерфейса (GTI или termios) и манипулирование escape-последовательностями с помощью tparm и родственных функций заключается в необходимости применения большого объема программного кода низкого уровня. Для многих программ предпочтительней интерфейс высокого уровня. Мы хотели бы иметь возможность просто рисовать на экране и применять библиотеку функций для автоматического отслеживания аппаратных характеристик терминала.

В этой главе вы узнаете именно о такой библиотеке, называемой curses. Стандарт curses очень важен как компромисс между простыми "строковыми" программами и полностью графическими (которые обычно труднее программировать) программами в графической оболочке X Window System, такими как GTK+/GNOME и Qt/KDE, В ОС Linux есть библиотека svgatib (Super VGA Library, библиотека низкоуровневой графики), но она не является стандартной библиотекой UNIX, поэтому обычно не доступна в других UNIX-подобных операционных системах.

Библиотека curses применяется во многих полноэкранных приложениях как довольно легкий и аппаратно-независимый способ разработки полноэкранных, хотя и символьных программ. Такие программы почти всегда легче писать с помощью библиотеки curses, чем непосредственно применять escape-последовательности. Эта библиотека также может управлять клавиатурой, обеспечивая легкий в использовании, не блокирующий режим ввода символов.

Вы можете столкнуться с тем, что несколько примеров из этой главы не всегда будут отображаться на простой консоли Linux так, как вы ожидали. Бывают случаи, когда сочетание библиотеки curses и определения консоли терминала получается немного не согласованным и приводит в результате к несколько странным компоновкам при использовании curses. Но если для отображения вывода применить графическую оболочку X Window System и окно xterm, все встанет на свои места.

В этой главе обсуждаются следующие темы:

□ применение библиотеки curses:

□ основные идеи curses;

□ управление базовыми вводом и выводом;

□ использование множественных окон;

□ применение режима дополнительной клавиатуры (keypad mode);

□ добавление цвета.

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

Компиляция с библиотекой curses

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

Поскольку curses — это библиотека, для ее применения необходимо включить в программу заголовочный файл, объявления функций и макросы из соответствующей системной библиотеки. Существует несколько разных реализаций библиотеки curses. Первоначальная версия появилась в системе BSD UNIX и затем была включена в разновидности UNIX стиля System V прежде, чем была стандартизована группой X/Open. Система Linux использует вариант ncurses ("new curses") — свободно распространяемую версию System V Release 4.0 curses, разработанную для Linux. Эта реализация хорошо переносится на другие версии UNIX, хотя и содержит несколько непереносимых дополнительных функций. Есть даже версии библиотеки для MS-DOS и Windows. Если вы увидите, что библиотека curses, поставляемая с вашей версией системы UNIX, не поддерживает некоторые функции, попытайтесь получить копию альтернативной библиотеки ncurses. Обычно пользователи ОС Linux обнаруживают уже установленную библиотеку ncurses или, по крайней мере, ее компоненты, необходимые для выполнения программ на базе библиотеки curses. Если инструментальные библиотеки для нее заранее не установлены в вашем дистрибутиве (нет файла curses.h или файла библиотеки curses для редактирования связей), для большинства основных дистрибутивов их всегда можно найти в виде стандартного пакета с именем наподобие ibncurses5-dev.

Примечание

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

При компиляции программ, использующих curses, следует подключить заголовочный файл curses.h и на этапе редактирования связей саму библиотеку с помощью аргумента -lcurses. Во многих системах Linux вы можете применять просто библиотеку curses, а потом обнаружить, что на самом деле вы пользуетесь усовершенствованной, более новой реализацией ncurses.

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

ls -l /usr/include/*curses.h

для просмотра заголовочных файлов и

ls -l /usr/lib/lib*curses*

для проверки библиотечных файлов. Если вы увидите, что curses.h и ncurses.h — прямо связанные файлы, и существует файл библиотеки ncurses, то у вас есть возможность компилировать файлы из этой главы с помощью следующей команды:

$ gcc program. с -о program -lcurses

Если установка curses в вашей системе не использует автоматически ncurses, вы сможете явно задать использование ncurses, включив файл ncurses.h вместо файла curses.h и выполнив следующую команду:

$ gcc -I/usr/include/ncurses program.с -о program -lncurses

в которой опция -I задает каталог для поиска заголовочного файла.

Примечание

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

Если вы точно не знаете, как установлена библиотека curses в вашей системе, обратитесь к страницам интерактивного справочного руководства, посвященным ncurses, или просмотрите другую интерактивную документацию; обычное место ее хранения — каталог /usr/share/doc/, в котором вы найдете каталог curses или ncurses часто с присоединенным в конце номером версии.

Терминология библиотеки curses и общие представления

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

Библиотека curses поддерживает две структуры данных, действующие как отображение экрана терминала: stdscr и curscr. Структура stdscr, наиболее важная из двух, обновляется, когда функции curses формируют вывод. Структура данных stdscr — "стандартный экран". Она действует во многом так же, как стандартный вывод stdout из библиотеки stdio. Эта структура — стандартное окно вывода в программах, использующих библиотеку curses. Структура curscr похожа на stdscr, но хранит внешний вид отображаемого в текущий момент экрана. Вывод, записанный в структуру stdscr, не появляется на экране до тех пор, пока программа не вызовет функцию refresh, в которой библиотека curses сравнивает содержимое stdscr (как должен выглядеть экран) со второй структурой curscr (как выглядит экран в данный момент). Затем curses использует различия между этими двумя структурами для обновления экрана.

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

Таким образом, процесс вывода символов в программе с применением curses выглядит следующим образом:

1. Используется функция библиотеки curses для обновления логического экрана.

2. Запрашивается у библиотеки curses обновление физического экрана с помощью функции refresh.

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

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

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

Рис. 6.1

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

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

Упражнение 6.1. Программа с использованием curses, выводящая приветствие

В этом примере вы напишите очень простую использующую curses программу screen1.c, чтобы показать эти и другие базовые функции в действии. Далее будут описаны их прототипы.

1. Вставьте заголовочный файл curses.h и в функцию main, включите вызовы для инициализации и возврата в исходное состояние библиотеки curses:

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

int main() {

 initscr();

 ...

 endwin();

 exit(EXIT_SUCCESS);

}

2. Внутрь поместите код для перемещения курсора в точку (5, 15) на логическом экране, выведите приветствие "Hello World" и обновите реальный экран. В заключение примените вызов sleep(2) для того, чтобы приостановить выполнение программы на две секунды и просмотреть вывод на экран перед ее завершением:

move(5, 15);

printw("%s", "Hello World");

refresh();

sleep(2);

Пока программа выполняется, вы видите фразу "Hello World" в левом верхнем квадранте пустого экрана (рис. 6.2).

Рис. 6.2

Как это работает

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

Экран

Как: вы уже видели, все программы с использованием curses должны начинаться с вызова функции initscr и заканчиваться вызовом функции endwin. Далее приведены их описания из заголовочного файла.

#include <curses.h>

WINDOW *initscr(void);

int endwin(void);

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

Функция endwin возвращает константу OK в случае успешного завершения и err в случае неудачи. Вы можете вызвать ее для того, чтобы покинуть curses, а позже возобновить функционирование библиотеки curses, вызвав clearok(stdscr, 1) и refresh. Это позволит библиотеке совершенно забыть, как выглядит физический экран, и заставит ее выполнить полное обновление экрана.

Вывод на экран

Для обновления экрана предоставляется несколько базовых функций.

#include <curses.h>

int addch(const chtype char_to_add);

int addchstr(chtype *const string_to_add);

int printw(char *format, ...);

int refresh(void);

int box(WINDOW *win_ptr, chtype vertical_char, chtype horizontal_char);

int insch(chtype char_to_insert);

int insertln(void);

int delch(void);

int deleteln(void);

int beep(void);

int flash(void);

У библиотеки curses есть свой символьный тип данных chtype, который может содержать больше разрядов, чем стандартный тип char. В стандартной версии ncurses для ОС Linux chtype на самом деле — синоним стандартного типа unsigned long.

Функции addch и addchstr вставляют заданные символ или строку в текущую позицию на экране. Функция printw форматирует строку так же, как функция printf, и помещает в текущую позицию на экране. Функция refresh вызывает обновление физического экрана, возвращая OK в случае успеха и ERR при возникновении ошибки. Функция box позволяет нарисовать рамку вокруг окна.

Примечание

В стандартной библиотеке curses вы можете применять только "обычные" символы для рисования горизонтальных и вертикальных линий. В расширенной версии библиотеки можно использовать два определения, ASC_VLINE и ACS_HLINE, для вывода символов вертикальных и горизонтальных линий соответственно, которые позволят нарисовать внешне более привлекательную рамку. Для этого ваш терминал должен поддерживать символы псевдографики. Обычно они лучше отображаются в окне эмулятора xterm, чем на стандартной консоли, но их поддержка полна корректировок или "заплат", поэтому мы полагаем, что вы откажетесь от их применения, если важна переносимость вашей программы.

Функция insch вставляет символ, сдвигая имеющиеся символы вправо. При этом не определено, что произойдет в конце строки, результат зависит от используемого терминала. Функция insertln вставляет пустую строку, перемещая имеющиеся строки на одну вниз. Функции delch и deleteln аналогичны функциям insert.

Для получения звука можно вызвать функцию beep. Немногие терминалы не способны издавать звуки, в этом случае некоторые установки библиотеки curses при вызове beep заставят экран мигать. Если вы работаете в густонаселенном офисе и звуковые сигналы могут издавать многие компьютеры, возможно, вы сочтете мигание предпочтительным режимом. Как и ожидалось, функция flash вызывает мигание экрана, если это невозможно, она попробует заставить терминал издать звуковой сигнал взамен.

Считывание с экрана

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

#include <curses.h>

chtype inch(void);

int instr(char *string);

int innstr(char *string, int number_of_characters);

Функция inch должна быть всегда доступна, а функции instr и innstr не всегда поддерживаются. Функция inch возвращает символ из текущей позиции курсора на экране и данные о его атрибутах. Обратите внимание на то, что функция возвращает значение не char, a chtype, в то время как функции instr и innstr пишут в массивы с элементами типа char.

Очистка экрана

Существует четыре основных способа очистки области экрана:

#include <curses.h>

int erase (void);

int clear(void);

int clrtobot(void);

int clrtoeol(void);

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

Функция clear обычно применяет команду терминала, которая очищает весь экран, а не пытается стереть текущие непробельные символы во всех точках экрана. Это делает функцию clear надежным средством очистки экрана. Сочетание функции clear с последующей функцией refresh может обеспечить удобную команду перерисовки экрана в том случае, когда изображение на экране беспорядочно или испорчено каким-либо образом.

Функция clrtobot очищает экран, начиная с текущей позиции курсора и далее до конца экрана, а функция clrtoeol очищает экран, начиная с текущей позиции курсора до конца строки, в которой находится курсор.

Перемещение курсора

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

#include <curses.h>

int move(int new_y, int new_x);

int leaveok(WINDOW *window_ptr, bool leave_flag);

Функция move просто переносит позицию логического курсора в заданное место на экране. Напоминаем о том, что начало экранных координат (0, 0) находится в левом верхнем углу экрана. В большинстве версий библиотеки curses две глобальные целочисленные переменные, LINES и COLUMNS, определяют размер физического экрана и могут применяться для определения максимально допустимых значений параметров new_y и new_x. Вызов move сам по себе не приводит к перемещению физического курсора. Он только изменяет позицию на логическом экране, в которой появится следующий вывод. Если вы хотите, чтобы экранный курсор переместился немедленно после вызова функции move, вставьте следом за ним вызов функции refresh.

Функция leaveok устанавливает флаг, управляющий положением курсора на физическом экране после его обновления. По умолчанию флаг равен false, и после вызова refresh аппаратный курсор остается в той же точке экрана, что и логический курсор. Если флаг равен true, аппаратный курсор можно оставить в случайно выбранной точке экрана. Как правило, значение, устанавливаемое по умолчанию, предпочтительней, т.к. курсор остается в не лишенной смысла позиции.

Атрибуты символов

У всех символов, обрабатываемых curses, могут быть определенные атрибуты, управляющие способом отображения символа на экране при условии, что оборудование, применяемое для их отображения, поддерживает требуемый атрибут. Определены следующие атрибуты: A_BLINK, A_BOLD, A_DIM, A_REVERSE, A_STANDOUT и A_UNDERLINE. Вы можете использовать перечисленные далее функции для установки атрибутов по одному или все вместе.

#include <curses.h>

int attron(chtype attribute);

int attroff(chtype attribute);

int attrset(chtype attribute);

int standout(void);

int standend(void);

Функция attrset устанавливает атрибуты curses, функции attron и attroff включают и отключают заданные атрибуты, не портя остальные, а функции standout и standend обеспечивают более выразительный режим выделения или "лучший из всех" режим. На большинстве терминалов выбирается инверсия.

Выполните упражнение 6.2.

Упражнение 6.2. Перемещение, вставка и атрибуты

Теперь, когда вы знаете больше об управлении экраном, можно испытать более сложный пример moveadd.c. Вы включите несколько вызовов функций refresh и sleep в этот пример, чтобы на каждом шаге видеть, как выглядит экран. Обычно программы с использованием библиотеки curses стараются обновлять экран как можно реже, поскольку это не слишком высокопроизводительная операция. Программный код написан с некоторой долей искусственности для обеспечения большей наглядности.

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

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>

#include <curses.h>

int main() {

 const char witch_one[] = " First Witch ";

 const char witch_two[] = " Second Witch ";

 const char *scan_ptr;

 initscr();

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

 move(5, 15);

 attron(A_BOLD);

 printw("%s", "Macbeth");

 attroff(A_BOLD);

 refresh();

 sleep(1);

 move(8, 15);

 attron(A_STANDOUT);

 printw("%s", "Thunder and Lightning");

 attroff(A_STANDOUT);

 refresh();

 sleep(1);

 move(10, 10);

 printw("%s", "When shall we three meet again");

 move(11, 23);

 printw("%s", "In thunder, lightning, or in rain ?");

 move(13, 10);

 printw("%s", "When the hurlyburly's done, ");

 move(14, 23);

 printw("%s", "When the battle's lost and won.");

 refresh();

 sleep(1);

3. Действующие лица идентифицированы, и их имена выводятся посимвольно:

 attron(A_DIM);

 scan_ptr = witch_one + strlen(witch_one) - 1;

 while (scan_ptr != witch_one) {

  move(10, 10);

  insch(*scan_ptr--);

 }

 scan_ptr = witch_two + strlen(witch_two) - 1;

 while (scan_ptr != witch_two) {

  move(13, 10);

  insch(*scan_ptr--);

 }

 attroff(A_DIM);

 refresh();

 sleep(1);

4. В заключение переместите курсор в правый нижний угол экрана, а затем подготовьте и выполните завершение:

 move(LINES - 1, COLS - 1);

 refresh();

 sleep(1);

 endwin();

 exit(EXIT_SUCCESS);

}

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

Рис. 6.3

Как это работает

После инициализации некоторых переменных и экрана с помощью библиотеки curses вы применили функции move для перемещения курсора по экрану. Посредством функций attron и attroff вы управляли атрибутами текста, выводимого в заданную точку экрана. Далее перед закрытием библиотеки curses и завершением программа продемонстрировала, как вставлять символы функцией insch.

Клавиатура

Наряду с предоставлением интерфейса, облегчающего управление экраном, библиотека curses также предлагает средства, облегчающие управление клавиатурой.

Режимы клавиатуры

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

#include <curses.h>

int echo(void);

int noecho(void);

int cbreak(void);

int nocbreak(void);

int raw(void);

int noraw(void);

Функции echo и noecho просто включают и отключают отображение символов, набираемых на клавиатуре. Оставшиеся четыре функции управляют тем, как символы, набранные на терминале, становятся доступны программе с применением curses. Для того чтобы понять функцию cbreak, необходимо иметь представление о стандартном режиме ввода. Когда программа, использующая библиотеку curses, стартует с вызова функции initscr, устанавливается режим ввода, называемый режимом с обработкой (cooked mode). Это означает построчную обработку, т.е. ввод становится доступен программе после нажатия пользователем клавиши <Enter> (или <Return> на некоторых клавиатурах). Специальные символы на клавиатуре включены, поэтому набор соответствующих клавиатурных последовательностей может сгенерировать сигнал в программе. Управление потоком, если терминал запускается с терминала, также включено. Вызывая функцию cbreak, программа может установить режим ввода cbreak, в котором символы становятся доступными программе сразу после их набора, а не помещаются в буфер и передаются программе только после нажатия клавиши <Enter>. Как и в режиме с обработкой, специальные символы клавиатуры действуют, а простые клавиши, например <Backspace>, передаются для обработки непосредственно в программу, поэтому если вы хотите, чтобы нажатие клавиши <Backspace> приводило к привычным действиям, то вы должны запрограммировать их самостоятельно.

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

Клавиатурный ввод

Чтение с клавиатуры — очень простая операция. К основным функциям чтения относятся следующие:

#include <curses.h>

int getch(void);

int getstr(char *string);

int getnstr(char *string, int number_of_characters);

int scanw(char *format, ...);

Все они действуют подобно своим аналогам, не входящим в библиотеку curses, getchar, gets и scanf. Обратите внимание на то, что у функции getstr нет возможности ограничить длину возвращаемой строки, поэтому применять ее следует с большой осторожностью. Если ваша версия библиотеки curses поддерживает функцию getnstr, позволяющую ограничить количество считываемых символов, всегда применяйте ее вместо функции getstr. Это очень напоминает поведение функций gets и fgets, с которыми вы познакомились в главе 3.

В упражнении 6.3 для демонстрации управления клавиатурой приведен пример короткой программы ipmode.c.

Упражнение 6.3. Режим клавиатуры и ввод

1. Наберите программу и включите в нее начальные вызовы библиотеки curses:

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

#include <string.h>

#define PW_LEN 256

#define NAME_LEN 256

int main() {

 char name[NAME_LEN];

 char password[PW_LEN];

 const char *real_password = "xyzzy";

 int i = 0;

 initscr();

 move(5, 10);

 printw("%s", "Please login:");

 move(7, 10);

 printw("%s", "User name: ");

 getstr(name);

 move(9, 10);

 printw("%s", "Password: ");

 refresh();

2. Когда пользователь вводит свой пароль, необходимо остановить отображение символов на экране. Далее сравните введенный пароль со строкой xyzzy:

 cbreak();

 noecho();

 memset(password, '\0', sizeof(password));

 while (i < PW_LEN) {

  password[i] = getch();

  if (password[i] == '\n') break;

  move(8, 20 + i);

  addch('*');

  refresh();

  i++;

 }

3. В заключение восстановите отображение символов и выведите сообщение об успешном или неудачном завершении:

 echo();

 nocbreak();

 move(11, 10);

 if (strncmp(real_password, password, strlen(real_password)) == 0)

   printw("%s", "Correct");

 else printw("%s", "Wrong");

 printw("%s", " password");

 refresh();

 sleep(2);

 endwin();

 exit(EXIT_SUCCESS);

}

Как это работает

Остановив отображение клавиатурного ввода и установив режим cbreak, вы выделяете область памяти, готовую к приему пароля. Каждый введенный символ пароля немедленно обрабатывается, и на экран выводится * в следующей позиции курсора. Вам необходимо каждый раз обновлять экран и сравнивать с помощью функции strcmp две строки: введенный и реальный пароли.

Примечание

Если вы пользуетесь очень старой версией библиотеки curses, вам, возможно, понадобится выполнить дополнительный вызов функции refresh перед вызовом функции getstr. В библиотеке ncurses вызов getstr обновляет экран автоматически.

Окна

До сих пор вы использовали терминал как средство полноэкранного вывода. Это вполне подходит для маленьких простых программ, но библиотека curses идет гораздо дальше. Вы можете на физическом экране одновременно отображать множество окон разных размеров. Многие из описанных в этом разделе функций поддерживаются в терминах стандарта X/Open так называемой "расширенной" версией curses. Но поскольку они поддерживаются библиотекой ncurses, не велика проблема сделать их доступными на большинстве платформ. Пора идти дальше и применить множественные окна. Вы увидите, как обобщаются до сих пор использовавшиеся команды и применяются в сценариях с множественными окнами.

Структура WINDOW

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

stdscr — это специальный случай структуры WINDOW, как stdout — специальный случай файлового потока. Обычно структура WINDOW объявляется в файле curses.h и, несмотря на то, что ее просмотр может быть очень поучителен, программы никогда не используют эту структуру напрямую, т.к. она может различаться в разных реализациях.

Вы можете создать и уничтожить окно с помощью вызовов функций newwin и delwin:

#include <curses.h>

WINDOW *newwin(int num_of_lines, int num_of_cols, int start_y, int start_x);

int delwin(WINDOW *window_to_delete);

Функция newwin создает новое окно в позиции экрана (start_y, int start_x) и с заданным. количеством строк и столбцов. Она возвращает указатель на новое окно или NULL, если создать окно невозможно. Если вы хотите, чтобы правый нижний угол нового окна совпадал с правым нижним углом экрана, можно задать нулевое количество строк и столбцов. Все окна должны располагаться в пределах экрана. Функция newwin завершится аварийно, если какая-либо часть окна окажется за пределами экрана. Новое окно, созданное newwin, абсолютно независимо от всех уже имеющихся окон. По умолчанию оно помещается поверх существующих окон, скрывая (но не изменяя) их содержимое.

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

Примечание

Следите за тем, чтобы никогда не было попыток удалить собственные окна библиотеки curses: stdscr и curscr!

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

Универсальные функции

Вы уже применяли функции addch и printw для вставки символов на экран. К этим функциям, как и ко многим другим, может быть добавлен префикс либо w для окна, либо mv для перемещения курсора, либо mvw для перемещения и окна. Если вы посмотрите заголовочный файл большинства версий библиотеки curses, то увидите, что многие функции, применявшиеся до сих пор, — простые макросы (#defines), вызывающие эти более универсальные функции.

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

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

Далее для примера приведен полный набор прототипов для семейств функций addch и printw.

#include <curses.h>

int addch(const chtype char);

int waddch(WINDOW *window_pointer, const chtype char);

int mvaddch(int y, int x, const chtype char);

int mvwaddch(WINDOW *window_pointer, int y, int x, const chtype char);

int printw(char *format, ...);

int wprintw(WINDOW *window_pointer, char *format, ...);

int mvprintw(int y, int x, char *format, ...);

int mvwprintw(WINDOW *window_pointer, int y, int x, char *format, ...);

У многих других функций, например inch, также есть варианты оконные и с перемещением курсора.

Перемещение и обновление окна

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

#include <curses.h>

int mvwin(WINDOW *window_to move, int new_y, int new x);

int wrefresh(WINDOW *window_ptr);

int wclear(WINDOW *window_ptr);

int werase(WINDOW *window_ptr);

int touchwin(WINDOW *window_ptr);

int scrollok(WINDOW *window_ptr, bool scroll_flag);

int scroll(WINDOW *window_ptr);

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

Функции wrefresh, wclear и werase — просто обобщения функций, с которыми вы встречались ранее; они только принимают указатель WINDOW, поэтому могут ссылаться на конкретное окно, а не на окно stdscr.

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

Две функции scroll управляют прокруткой окна. Функция scrollok при передаче логического значения true (обычно ненулевого) включает прокрутку окна. По умолчанию окна не прокручиваются. Функция scroll просто прокручивает окно на одну строку вверх. В некоторые реализации библиотеки curses входит и функция wsctl, которая также принимает количество строк для прокрутки, которое может быть и отрицательным числом. Мы вернемся к прокрутке немного позже в этой главе.

А теперь выполните упражнение 6.4.

Упражнение 6.4. Управление множественными окнами

Теперь, зная, как управлять несколькими окнами, вы можете включить эти новые функции в программу multiw1.c. Для краткости проверка ошибок не приводится.

1. Как обычно, вставьте первыми отсортированные объявления:

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

int main() {

 WINDOW *new_window_ptr;

 WINDOW *popup_windov_ptr;

 int x loop;

 int y_loop;

 char a_letter = 'a';

 initscr();

2. Заполните базовое окно символами, обновляя физический экран, когда заполнен логический экран:

 move(5, 5);

 printw("%s", "Testing multiple windows");

 refresh();

 for (y_loop = 0; y_loop < LINES - 1; y_loop++) {

  for (x_loop = 0; x_loop < COLS - 1; x_loop++) {

   mvwaddch(stdscr, y_loop, x_loop, a_letter);

   a_letter++;

   if (a_letter > 'z') a_letter = 'a';

  }

 }

 /* Обновление экрана */

 refresh();

 sleep(2);

3. Теперь создайте окно 10×20 и вставьте в него текст перед прорисовкой окна на экране:

 new_window_ptr = newwin(10, 20, 5, 5);

 mvwprintw(new_window_ptr, 2, 2, "%s", "Hello World");

 mwwprintw(new_window_ptr, 5, 2, "%s",

  "Notice how very long lines wrap inside the window");

 wrefresh(new_window_ptr);

 sleep(2);

4. Измените содержимое фонового окна. Когда вы обновите экран, окно, на которое указывает new_window_ptr, будет затемнено:

 a_letter = '0';

 for (y_lоор = 0; y_lоор < LINES - 1; y_lоор++) {

  for (х_lоор = 0; xloop < COLS - 1; х_lоор++) {

   mvwaddch(stdscr, y_loop, х_lоор, a_letter);

   a_letter++;

   if (a_letter > '9') a_letter = '0';

  }

 }

 refresh();

 sleep(2);

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

 wrefresh(new_window_ptr);

 sleep(2);

6. Но если вы сначала воспользуетесь функцией touchwin и заставите библиотеку curses думать, что окно было изменено, следующий вызов функции wrefresh снова отобразит новое окно на переднем плане.

 touchwin(new_window_ptr);

 wrefresh(new_window_ptr);

 sleep(2);

7. Добавьте еще одно накладывающееся окно с рамкой вокруг него.

 popup_window_ptr = newwin(10, 20, 8, 8);

 box(popup_window_ptr, '|', '-');

 mvwprintw(popup_window_ptr, 5, 2, "%s", "Pop Up Window!");

 wrefresh(popup_window_ptr);

 sleep(2);

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

 touchwin(new_window_ptr);

 wrefresh(new_window_ptr);

 sleep(2);

 wclear(new_window_ptr);

 wrefresh(new_window_ptr);

 sleep(2);

 delwin(new_window_ptr);

 touchwin(popup_window_ptr);

 wrefresh(popup_window_ptr);

 sleep(2);

 delwin(popup_window_ptr);

 touchwin(stdscr);

 refresh();

 sleep(2);

 endwin();

 exit(EXIT_SUCCESS);

}

К сожалению, нет возможности продемонстрировать выполнение этого фрагмента в книге, но на рис. 6.4 показан снимок экрана после отображения первого всплывающего окна.

Рис. 6.4

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

Рис. 6.5

Как это работает

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

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

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

Примечание

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

Оптимизация обновлений экрана

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

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

#include <curses.h>

int wnoutrefresh(WINDOW *window_ptr);

int doupdate(void);

Функция wnoutrefresh определяет, какие символы необходимо отправить на экран, но не отправляет их на самом деле. Функция doupdate действительно отправляет изменения на терминал. Если вы просто вызовите wnoutrefresh, а за ней тут же функцию doupdate, эффект будет такой же, как при вызове функции wrefresh. Однако если вы хотите перерисовать ряд окон, то можете вызвать функцию wnoutrefresh для каждого окна (конечно, в нужном порядке) и затем вызвать doupdate только после последнего вызова wnoutrefresh. Это позволит библиотеке curses выполнить расчеты, связанные с обновлением экрана, по очереди для каждого окна и только после этого вывести обновленный экран. Такой подход почти всегда позволяет curses минимизировать количество символов, нуждающихся в пересылке.

Вложенные окна

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

#include <curses.h>

WINDOW *subwin(WINDOW *parent, int num_of_lines, int num_of_cols,

 int start_y, int start_x);

int delwin(WINDOW *window_to_delete);

У функции subwin почти такой же список параметров, как у функции newwin, и удаляются вложенные окна так же, как другие окна с помощью вызова delwin. Для записи во вложенные окна, как и в новые окна, вы можете применять ряд функций mvw. На самом деле большую часть времени вложенные окна ведут себя почти так же, как новые окна, но есть одно важное отличие: подокна самостоятельно не хранят отдельный набор экранных символов; они используют ту же область хранения символов, что и родительское окно, заданное при создании вложенного окна. Это означает, что любые изменения, сделанные во вложенном окне, вносятся и в лежащее в основании родительское окно, поэтому, когда подокно удаляется, экран не меняется.

На первый взгляд вложенные окна кажутся бесполезным экспериментом. Почему не изменять просто родительское окно? Основная сфера их применения — предоставление простого способа прокрутки другого окна. Потребность в прокрутке небольшой области экрана удивительно часто возникает при написании программ с использованием curses. Создав вложенное окно и прокручивая его, вы добьетесь желаемого результата.

Примечание

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

Выполните упражнение 6.5.

Упражнение 6.5. Вложенные окна

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

1. Начальная секция кода программы subscl.c инициализирует отображение базового окна с некоторым текстом:

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

int main() {

 WINDOW *sub_window_ptr;

 int x_loop;

 int y_loop;

 int counter;

 char a_letter = '1';

 initscr();

 for (y_loop = 0; y_loop < LINES - 1; y_loop++) {

  for (x_loop = 0; x_loop < COLS - 1; x_loop++) {

   mvwaddch(stdscr, y_loop, x_loop, a_letter);

   a_letter++;

   if (a_letter > '9') a_letter = '1';

  }

 }

2. Теперь создайте новое подокно с прокруткой. Как рекомендовалось, вам следует перед обновлением экрана "коснуться" родительского окна:

 ub_window_ptr = subwin(stdscr, 10, 20, 10, 10);

 scrollok(sub_window_ptr, 1);

 touchwin(stdscr);

 refresh();

 sleep(1);

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

 werase(sub_window_ptr);

 mvwprintw(sub_window_ptr, 2, 0, "%s", "This window will now scroll");

 wrefresh(sub_window_ptr);

 sleep(1);

 for (counter = 1; counter < 10; counter++) {

  wprintw(sub_window_ptr, "%s", "This text is both wrapping and \

   scrolling.");

  wrefresh(sub_window_ptr);

  sleep(1);

 }

4. Завершив цикл, удалите вложенное окно и обновите основной экран:

 delwin(sub_window_ptr);

 touchwin(stdscr);

 refresh();

 sleep(1);

 endwin();

 exit(EXIT_SUCCESS);

}

К концу программы вы увидите вывод, показанный на рис. 6.6.

Рис. 6.6 

Как это работает

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

Дополнительная клавиатура

Вы уже познакомились с некоторыми средствами библиотеки curses для обработки клавиатурного ввода. У многих клавиатур, как минимум, есть клавиши управления курсором и функциональные клавиши. Кроме того, у многих клавиатур есть дополнительная клавиатура и другие клавиши, например, <Insert> и <Home>.

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

К счастью, библиотека curses предоставляет элегантное решение для управления функциональными клавишами. Обычно в структуре terminfo для каждого терминала хранится последовательность, отправляемая каждой функциональной клавишей, и во включенном в программу файле curses.h для логических клавиш есть набор определений, начинающихся с префикса KEY_.

Когда curses стартует, преобразование последовательностей в логические клавиши отключено, и его следует включить вызовом функции keypad. Если вызов успешен, функция вернет OK, в противном случае ERR.

#include <curses.h>

int keypad(WINDOW *window_ptr, bool keypad_on);

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

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

□ Распознавание escape-последовательностей требует разного времени, и многие сетевые протоколы сгруппируют символы в пакеты (что приведет к неверному распознаванию escape-последовательностей) или разделят их (что приведет к распознаванию последовательностей функциональных клавиш, как клавиши <Esc> и отдельных символов). Такое поведение чаще всего наблюдается в региональных сетях (Wide-Area Network, WAN) и других медленных линиях связи. Единственный выход — попытаться запрограммировать терминалы так, чтобы они отправляли единичные уникальные символы в ответ на нажатие каждой функциональной клавиши, используемой вами, хотя это ограничит количество управляющих символов.

□ Для того чтобы библиотека curses могла отличить нажатие клавиши <Esc> от клавиатурной последовательности, начинающейся с символа Esc, ей требуется ожидание в течение короткого промежутка времени. Иногда при включенном режиме дополнительной клавиатуры можно заметить легкую задержку при обработке клавиши <Esc>.

□ Библиотека curses не может обрабатывать неуникальные escape-последовательности. Если у вашего терминала есть две разные клавиши, отправляющие одну и ту же последовательность, библиотека просто не будет ее обрабатывать, поскольку не может решить, какую логическую клавишу следует вернуть.

Выполните упражнение 6.6.

Упражнение 6.6. Применение дополнительной клавиатуры

Далее приведена короткая программа keypad.c, демонстрирующая применение режима дополнительной клавиатуры. После запуска программы нажмите клавишу <Esc> и отметьте незначительную задержку, в течение которой программа пытается понять: Esc — это начало управляющей последовательности или просто нажатие одной клавиши,

1. Инициализировав программу и библиотеку curses, включите режим дополнительной клавиатуры:

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

#define LOCAL_ESCAPE_KEY 27

int main() {

 int key;

 initscr();

 crmode();

 keypad(stdscr, TRUE);

2. Отключите отображение символов, чтобы помешать перемещению курсора при нажатии клавиш управления курсором. Экран очищается, и выводится некоторый текст. Программа ждет нажатия клавиши и до тех пор, пока не нажата клавиша <Q> или не возникла ошибка. Символ нажатой клавиши выводится на экран. Если нажатые клавиши соответствуют одной из последовательностей для дополнительной клавиатуры терминала, вместо символа выводится эта последовательность.

 noecho();

 clear();

 mvprintw(5, 5, "Key pad demonstration. Press 'q' to quit");

 move(7, 5);

 refresh();

 key = getch();

 while (key != ERR && key i= 'q') {

  move(7, 5);

  clrtoeol();

  if ((key >= 'A' && key <= 'Z') || (key >= 'a' && key <= 'z')) {

   printw("Key was%c", (char)key);

  } else {

   switch(key) {

   case LOCAL_ESCAPE_KEY:

    printw("%s", "Escape key");

    break;

   case KEY_END:

    printw("%s", "END key");

    break;

   case KEY_BEG:

    printw("%s", "BEGINNING key");

    break;

   case KEY_RIGHT:

    printw("%s", "RIGHT key");

    break;

   case KEY_LEFT:

    printw("%s", "LEFT key");

    break;

   case KEY_UP:

    printw("%s", "UP key");

    break;

   case KEY_DOWN:

    printw("%s", "DOWN key");

    break;

   default:

    printw("Unmatched — %d", key);

    break;

   } /* switch */

  } /* else */

  refresh();

  key = getch();

 } /* while */

 endwin();

 exit(EXIT_SUCCESS);

}

Как это работает

Включив режим дополнительной клавиатуры, вы увидите, как можно распознать различные функциональные клавиши на дополнительной клавиатуре, генерирующие escape-последовательности. Вы, возможно, сумеете заметить, что распознавание клавиши <Esc> немного медленнее, чем других клавиш.

Применение цвета

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

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

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

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

#include <curses.h>

bool has_colors(void);

int start_color(void);

Функция has_colors возвращает true, если терминал поддерживает цвета. Далее следует вызвать функцию start_color, которая вернет OK, если цветовая поддержка успешно инициализирована. После вызова start_color и инициализации цветов переменная COLOR_PAIRS принимает значение, равное максимальному количеству цветовых пар, которые может поддерживать терминал. Переменная COLORS определяет максимальное число доступных цветов, которых, как правило, восемь. Внутри компьютера числа от 0 до 63 действуют как уникальные ID для каждого из доступных цветов.

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

#include <curses.h>

int init_pair(short pair_number, short foreground, short background);

int COLOR_PAIR(int pair_number);

int pair_content(short pair_number, short *foreground, short *background);

В файле curses.h обычно определены некоторые базовые цвета, начинающиеся с префикса COLOR_. Дополнительная функция pair_content позволяет извлечь сведения о ранее определенной цветовой паре.

Для определения цветовой пары номер 1, как красный на зеленом, примените следующую строку:

init_pair(1, COLOR_RED, COLOR_GREEN);

Затем вы сможете получить доступ к этой цветовой паре, применив функцию COLOR_PAIR следующим образом:

wattron(window_ptr, COLOR_PAIR(1));

Она установит вывод в будущем на экран красных символов на зеленом фоне.

Поскольку COLOR_PAIR — это атрибут, вы можете комбинировать его с другими атрибутами. На ПК часто можно добиться на экране цветов повышенной яркости, объединив с помощью поразрядной операции OR атрибут COLOR_PAIR с дополнительным атрибутом A_BOLD:

wattron(window_ptr, COLOR_PAIR(1) | A_BOLD);

Давайте проверим эти функции в примере color.c (упражнение 6.7).

Упражнение 6.7. Цвета

1. Сначала проверьте, поддерживает ли цвета терминал, используемый программой. Если да, то инициализируйте отображение цветов:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <curses.h>

int main() {

 int i;

 initscr();

 if (!has_colors()) {

  endwin();

  fprintf(stderr, "Error — no color support on this terminal\n");

  exit(1);

 }

 if (start_color() != OK) {

  endwin();

  fprintf(stderr, "Error — could not initialize colors\n");

  exit(2);

 }

2. Теперь можно вывести допустимое количество цветов и цветовые пары. Создайте семь цветовых пар и выведите их по очереди на экран:

 clear();

 mvprintw(5, 5, "There are %d COLORS, and %d COLOR_PAIRS available", COLORS, COLOR_PAIRS);

 refresh();

 init_pair(1, COLOR_RED, COLOR_BLACK);

 init_pair(2, COLOR_RED, COLOR_GREEN);

 init_pair(3, COLOR_GREEN, COLOR_RED);

 init_pair(4, COLOR_YELLOW, COLOR_BLUE);

 init_pair(5, COLOR_BLACK, COLOR_WHITE);

 init_pair(6, COLOR_MAGENTA, COLOR_BLUE);

 init_pair(7, COLOR_CYAN, COLOR_WHITE);

 for (i = 1; i <= 7; i++) {

  attroff(A_BOLD);

  attrset(COLOR_PAIR(i));

  mvprintw(5 + i, 5, "Color pair %d", i);

  attrset(COLOR_PAIR(i) | A_BOLD);

  mwprintw(5 + i, 25, "Bold color pair %d", i);

  refresh();

  sleep(1);

 }

 endwin();

 exit(EXIT_SUCCESS);

}

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

Рис. 6.7

Как это работает

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

Переопределение цветов

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

#include <curses.h>

int init_color(short color_number, short red, short green, short blue);

Она позволяет переопределить существующий цвет (в диапазоне от 0 до COLORS) новыми значениями яркости цвета из диапазона от 0 до 1000. Такой подход немного напоминает определение цветовых характеристик в графических файлах формата GIF.

Панели 

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

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

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

Панели создаются во многом так же, как и обычные окна.

#include <curses.h>

WINDOW *newpad(int number_of_lines, int number_of_columns);

Обратите внимание на то, что возвращаемое значение — указатель на структуру типа WINDOW, такое же, как у функции newwin. Удаляются панели, как и окна, функцией delwin.

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

#include <сurses.h>

int prefresh(WINDOW *pad_ptr, int pad_row, int pad_column, int screen_row_min, int screen_col_min, int screen_row_max, int screen_соl_max);

Функция выполняет запись области панели, начинающейся в точке (pad_row, pad_column), в область экрана, определенную от (screen_row_min, screen_col_min) до (screen_row_max, screen_col_max).

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

Давайте проверим это на практике с помощью программы pad.с (упражнение 6.8).

Упражнение 6.8. Применение панели

1. В начале этой программы вы инициализируете структуру панели и затем формируете панель с помощью функции, которая возвращает указатель на нее. Вставьте символы, заполняющие структуру панели (панель на 50 символов шире и выше экрана терминала):

#include <unistd.h>

#include <stdlib.h>

#include <curses.h>

int main() {

 WINDOW *pad_ptr;

 int x, y;

 int pad_lines;

 int pad_cols;

 char disp_char;

 initscr();

 pad_lines = LINES + 50;

 pad_cols = COLS + 50;

 pad_ptr = newpad(pad_lines, padcols);

 disp_char = 'a';

 for (x = 0; x < pad_lines; x++) {

  for (у = 0; у < pad_cols; y++) {

   mvwaddch(pad_ptr, x, y, disp_char);

   if (disp_char == 'z') disp_char = 'a';

   else disp_char++;

  }

 }

2. Теперь перед завершением программы нарисуйте разные области панели в разных местах экрана:

 prefresh(pad_ptr, 5, 7, 2, 2, 9, 9);

 sleep(1);

 prefresh(pad_ptr, LINES + 5, COLS + 7, -5, 5, 21, 19);

 sleep(1);

 delwin(pad_ptr);

 endwin();

 exit(EXIT_SUCCESS);

}

Выполнив эту программу, вы увидите нечто подобное показанному на рис. 6.8.

Рис. 6.8

Приложение, управляющее коллекцией компакт-дисков

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

Все приложение занимает восемь страниц, поэтому мы разделили его на секции и отдельные функции внутри секций. Исходный код программы curses_app.c можно получить на Web-сайте издательства Wrox (http://www.wrox.com/WileyCDA/). Как и все программы из этой книги, оно подчиняется требованиям Общедоступной лицензии проекта GNU.

Примечание

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

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

Начало нового приложения для работы с коллекцией компакт-дисков

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

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

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <curses.h>

#define MAX_STRING 80 /* Самый длинный допустимый ответ */

#define MAX_ENTRY 1024 /* Самый длинный допустимый элемент БД */

#define MESSAGE_LINE 6 /* В этой строке разные сообщения */

#define ERROR LINE 22 /* Строка для вывода ошибок */

#define Q_LINE 20 /* Строка для вопросов */

#define PROMPT_LINE 18 /* Строка для вывода приглашения */

2. Теперь вам нужны глобальные переменные. Переменная current_cdis применяется для хранения названия текущего компакт-диска, с которым вы работаете в данный момент. Она инициализируется так, что первый символ равен NULL, чтобы показать, что компакт-диск не выбран. Символ завершения \0, строго говоря, не обязателен, но он гарантирует инициализацию переменной, что само по себе хорошая вещь. Переменная current_cat применяется для записи номера текущего компакт-диска в каталоге:

static char current_cd[MAX_STRING] = "\0";

static char current_cat[MAX_STRING];

3. Теперь объявите имена файлов. Для простоты в этой версии имена файлов фиксированные, как и имя временного файла.

Это может вызвать проблемы, если программа выполняется двумя пользователями в одном и том же каталоге. Лучше получать имена файлов базы данных как аргументы программы или из переменных окружения. Нам также потребуется улучшенный метод генерации уникального имени временного файла, для чего мы могли бы использовать функцию tmpnam из стандарта POSIX. Мы обратимся к решению многих из этих проблем в главе 8, когда применим СУРБД MySQL для хранения данных.

const char *title_file = "title.cdb";

const char *tracks_file = "tracks.cdb";

const char *temp_file = "cdb.tmp";

4. И наконец, прототипы функций:

void clear_all_screen(void);

void get_return(void);

int get_confirm(void);

int getchoice(char *greet, char *choices[]);

void draw_menu(char *options[], int highlight,

 int start_row, int start_col);

void insert_title(char *cdtitle);

void get_string(char *string);

void add_record(void);

void count_cds(void);

void find_cd(void);

void list_tracks(void);

void remove_tracks(void);

void remove_cd(void);

void update_cd(void);

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

char *main_menu[] = {

 "add new CD",

 "find CD",

 "count CDs and tracks in the catalog",

 "quit",

 0,

};

char *extended_menu[] = {

 "add new CD",

 "find CD",

 "count CDs and tracks in the catalog",

 "list tracks on current CD";

 "remove current CD",

 "update track information",

 "quit",

 0,

};

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

□ отображение меню;

□ добавление компакт-дисков в базу данных;

□ извлечение и отображение данных компакт-диска.

Визуальное представление дано на рис. 6.9.

Рис. 6.9

Взгляд на функцию main

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

int main() {

 int choice;

 initscr();

 do {

  choice = getchoice("Options:", current_cd[0] ? extended_menu : main_menu);

  switch (choice) {

  case 'q':

   break;

  case 'a':

   add_record();

   break;

  case 'c':

   count_cds();

   break;

  case 'f':

   find_cd();

   break;

  case 'l':

   list_tracks();

   break;

  case 'r':

   remove_cd();

   break;

  case 'u':

   update_cd();

   break;

  }

 } while (choice != 'q');

 endwin();

 exit(EXIT_SUCCESS);

}

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

Формирование меню

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

1. Функция getchoice, вызываемая из функции main, — это основная функция данной секции. В функцию getchoice передается приглашение greet и указатель choices на базовое или расширенное меню (в зависимости от того, выбран ли компакт-диск). Вы также увидите, как main_menu или extended_menu передаются как параметры в описанную ранее функцию main.

int get_choice(char *greet, char* choises[]) {

 static int selected_row = 0;

 int max_row = 0;

 int start_screenrow = MESSAGE_LINE, start_screencol = 10;

 char **option;

 int selected;

 int key = 0;

 option = choices;

 while (*option) {

  max_row++;

  option++;

 }

 if (selected_row >= max_row)

  selected_row = 0;

 clear_all_screen();

 mvprintw(start_screenrow - 2, start_screencol, greet);

 keypad(stdscr, TRUE);

 cbreak();

 noecho();

 key = 0;

 while (key != 'q' && key != KEY_ENTER && key != '\n') {

  if (key == KEY_UP) {

   if (selected_row == 0) selected_row = max_row - 1;

   else selected_row--;

  }

  if (key == KEY_DOWN) {

   if (selected_row == (max_row - 1)) selected_row = 0;

   else selected_row++;

  }

  selected = *choices[selected_row];

  draw_menu(choices, selected_row, start_screen_row, start_screencol);

  key = getch();

 }

 keypad(stdscr, FALSE);

 nocbreak();

 echo();

 if (key == 'q') selected = 'q';

 return(selected);

}

2. Обратите внимание на то, как две локальные функции clear_all_screen и draw_menu вызываются внутри функции getchoice. Первой рассмотрим функцию draw_menu:

void draw_menu(char* options[], int current_highlight, int start_row, int start_col) {

 int current_row = 0;

 char **option_ptr;

 char *txt_ptr;

 option_ptr = options;

 while (*option_ptr) {

  if (current_row == current_highlight) attron(A_STANDOUT);

  txt_ptr = options[current_row];

  txt_ptr++;

  mvprintw(start_row + current_row, start_col, "%s", txt_ptr);

  if (current_row == current_highlight) attroff(A_STANDOUT);

  current_row++;

  option_ptr++;

 }

 mvprintw(start_row + current_row + 3, start_col,

  "Move highlight then press Return ");

 refresh();

}

3. Далее рассмотрим функцию clear_all_screen, которая, как ни странно, очищает экран и перезаписывает заголовок. Если компакт-диск выбран, отображаются его данные:

void clear all_screen() {

 clear();

 mvprintw(2, 20, "%s", "CD Database Application");

 if (current_cd[0]) {

  mvprintw(ERROR_LINE, 0, "Current CD: %s: %s\n", current_cat, current_cd);

 }

 refresh();

}

Управление базой данных

В этом разделе описаны функции пополнения или обновления базы данных компакт-дисков. Функции add_record, update_cd и remove_cd вызываются из функции main.

Добавление записей

1. Добавьте сведения о новом компакт-диске в базу данных.

void add_record {

 char catalog_number[MAX_STRING];

 char cd_title[MAX_STRING];

 char cd_type[MAX_STRING];

 char cd_artist[MAX_STRING];

 char cd_entry[MAX_STRING];

 int screenrow = MESSAGE_LINE;

 int screencol = 10;

 clear_all_screen();

 mvprintw(screenrow, screencol, "Enter new CD details");

 screenrow += 2;

 mvprintw(screenrow, screencol, "Catalog Number: " );

 get_string(catalog_number);

 screenrow++;

 mvprintw(screenrow, screencol, " CD Title: ");

 get_string(cd_title);

 screenrow++;

 mvprintw(screenrow, screencol, " CD Type: ");

 get_string(cd_type);

 screenrow++;

 mvprintw(screenrow, screencol, " Artist: ");

 get_string(cd_artist);

 screenrow++;

 mvprintw(PROMPT_LINE-2, 5, "About to add this new entry:");

 sprintf(cd_entry, "%s, %s, %s, %s",

  catalog_number, cd_title, cd_type, cd_artist);

 mvprintw(PROMPT_LINE, 5, "%s", cd_entry);

 refresh();

 move(PROMPT_LINE, 0);

 if (get_confirm()) {

  insert_title(cd_entry);

  strcpy(current_cd, cd_title);

  strcpy(current_cat, catalog_number);

 }

}

2. Функция get_string приглашает к вводу и считывает строку из текущей позиции экрана. Она также удаляет завершающую новую пустую строку:

void get_string(char* string) {

 int len;

 wgetnstr(stdscr, string, MAX_STRING);

 len = strlen(string);

 if (len > 0 && string[len - 1] == '\n') string[len - 1] = '\0';

}

3. Функция get_confirm запрашивает и считывает пользовательское подтверждение. Она читает введенную пользователем строку и проверяет, первый символ — Y или у. Если она обнаруживает другой символ, то не дает подтверждения.

int get_confirm() {

 int confirmed = 0;

 char first_char;

 mvprintw(Q_LINE, 5, "Are you sure? ");

 clrtoeol();

 refresh();

 cbreak();

 first_char = getch();

 if (first_char == 'Y' || first_char == 'y') {

  confirmed = 1;

 }

 nocbreak();

 if (!confirmed) {

  mvprintw(Q_LINE, 1, " Cancelled");

  clrtoeol();

  refresh();

  sleep(1);

 }

 return confirmed;

}

4. Последней рассмотрим функцию insert_title. Она вставляет в базу данных компакт-дисков заголовок, добавляя строку с заголовком в конец файла заголовков:

void insert_title(char* cdtitle) {

 FILE *fp = fopen(title_file, "a");

 if (!fp) {

  mvprintw(ERROR_LINE, 0, "cannot open CD titles database");

 } else {

  fprintf(fp, "%s\n", cdtitle);

  fclose(fp);

 }

}

Обновление записей

1. Продолжим рассмотрение других управляющих функций, вызываемых из функции main. Следующая из них — функция update_cd. Эта функция использует обведенное рамкой вложенное окно с прокруткой и нуждается в нескольких константах, которые объявляются как глобальные, поскольку они позже потребуются функции list_tracks.

#define BOXED_LINES  11

#define BOXED_ROWS   60

#define BOX_LINE_POS 8

#define BOX_ROW_POS  2

2. Функция update_cd позволяет пользователю заново ввести сведения о дорожках текущего компакт-диска. Удалив предыдущие записи о дорожках, она приглашает ввести новую информацию.

void update_cd() {

 FILE *tracks_fp;

 char track_name[MAX_STRING];

 int len;

 int track = 1;

 int screen_line = 1;

 WINDOW *box_window_ptr;

 WINDOW *sub_window_ptr;

 clear_all_screen();

 mvprintw(PROMPT_LINE, 0, "Re-entering tracks for CD. ");

 if (!get_confirm())

return;

 move(PROMP_TLINE, 0);

 clrtoeol();

 remove_tracks();

 mvprintw(MESSAGE_LINE, 0, "Enter a blank line to finish");

 tracks_fp = fopen(tracks_file, "a");

Примечание

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

 box_window_ptr = subwin(stdscr, BOXED_LINES + 2, BOXED_ROWS + 2,

  BOX_LINE_POS - 1, BOX_ROW_POS - 1);

 if (!box_window_ptr) return;

 box(box_window_ptr, ACS_VLINE, ACS_HLINE);

 sub_window_ptr = subwin(stdscr, BOXED_LINES, BOXED_ROWS,

  BOX_LINE_POS, BOX_ROW_POS);

 if (!sub_window_ptr) return;

 scrollok(sub_window_ptr, TRUE);

 werase(sub_window_ptr);

 touchwin(stdscr);

 do {

  mvwprintw(sub_window_ptr, screen_line++, BOX_ROW_POS + 2,

   "Track %d: ", track);

  clrtoeol();

  refresh();

  wgetnstr(sub_window_ptr, track_name, MAX_STRING);

  len = strlen(track_name);

  if (len > 0 && track_name[len - 1] = '\n')

   track_name[len - 1] = '\0';

  if (*track_name)

   fprintf(tracks_fp, "%s, %d, %s\n", current_cat, track, track_name);

  track++;

  if (screen_line > BOXED__LINES - 1) {

   /* время начать прокрутку */

   scroll(sub_window_ptr);

   screen_line--;

  }

 } while (*track_name);

 delwin(sub_window_ptr);

 fclose(tracks_fp);

}

Удаление записей

1. remove_cd — последняя функция, вызываемая из функции main.

void remove_cd() {

 FILE *titles_fp, *temp_fp;

 char entry[MAX_ENTRY];

 int cat_length;

 if (current_cd[0] == '\0') return;

 clear_all_screen();

 mvprintw(PROMPT_LINE, 0, "About to remove CD %s: %s. ", current_cat, current_cd);

 if (!get_confirm())

  return;

 cat_length = strlen(current_cat);

 /* Файл заголовков копируется во временный, игнорируя данный CD */

 titles_fp = fopen(title_file, "r");

 temp_fp = fopen(temp_flie, "w");

 while(fgets(entry, MAX_ENTRY, titles_fp)) {

  /* Сравнивает номер в каталоге и копирует элемент, если не

     найдено совпадение */

  if (strncmp(current_cat, entry, cat_length) != 0)

   fputs(entry, temp_fp);

 }

 fclose(titles_fp);

 fclose(temp_fp);

 /* Удаляет файл заголовков и переименовывает временный файл */

 unlink(title_file);

 rename(temp_file, title_file);

 /* Теперь делает то же самое для файла дорожек */

 remove_tracks();

 /* Устанавливает 'None' для текущего CD */

 current_cd[0] = '\0';

}

2. Теперь вам только нужен программный код функции remove_tracks, удаляющей дорожки текущего компакт-диска. Она вызывается двумя функциями — update_cd и remove_cd.

void remove_tracks() {

 FILE *tracks_fp, *temp_fp;

 char entry[MAX_ENTRY];

 int cat_length;

 if (current_cd[0] == '\0') return;

 cat_length = strlen(current_cat);

 tracks_fp = fopen(tracks_file, "r");

 if (tracks_fp == (FILE *)NULL) return;

 temp_fp = fopen(temp_file, "w");

 while (fgets(entry, MAX_ENTRY, tracks_fp)) {

  /* Сравнивает номер в каталоге и копирует элемент, если не

     найдено совпадение */

  if (strncmp(current_cat, entry, cat_length) != 0)

   fputs(entry, temp_fp);

 }

 fclose(tracks_fp);

 fclose(temp_fp);

 /* Удаляет файл дорожек и переименовывает временный файл */

 unlink(tracks_file);

 rename(temp_file, tracks_file);

}

Запросы к базе данных компакт-дисков

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

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

void count_cds() {

 FILE *titles_fp, *tracks_fp;

 char entry[MAX_ENTRY];

 int titles = 0;

 int tracks = 0;

 titles_fp = fopen(title_file, "r");

 if (titles_fp) {

  while (fgets(entry, MAX_ENTRY, titles_fp))

   titles++;

  fclose(titles_fp);

 }

 tracks_fp = fopen(tracks_file, "r");

 if (tracks_fp) {

  while (fgets(entry, MAX_ENTRY, tracks_fp))

   tracks++;

  fclose(tracks_fp);

 }

 mvprintw(ERROR_LINE, 0,

  "Database contains %d titles, with a total of %d tracks.", titles, tracks);

 get_return();

}

2. Вы потеряли аннотацию к вашему любимому компакт-диску? Не волнуйтесь! Если вы аккуратно ввели подробную информацию в базу данных, теперь можно найти перечень дорожек с помощью функции find_cd. Она предлагает ввести подстроку, совпадение с которой нужно искать в базе данных, и устанавливает в глобальную переменную current_cd заголовок найденного компакт-диска.

void find_cd() {

 char match[MAX_STRING], entry[MAX_ENTRY];

 FILE *titles_fp;

 int count = 0;

 char *found, *title, *catalog;

 mvprintw(Q_LINE, 0, "Enter a string to search for in CD titles: ");

 get_string(match);

 titles_fp = fopen(title_file, "r");

 if (titles_fp) {

  while (fgets(entry, MAX_ENTRY, titles_fp)) {

   /* Пропускает прежний номер в каталоге */

   catalog = entry;

   if (found == strstr(catalog, ", ")) {

    *found = '\0';

    title = found + 1;

    /* Стирает следующую запятую в элементе, укорачивая его

       только до заголовка */

    if (found == strstr(title, ", ")) {

     *found = '\0';

     /* Теперь проверяет, есть ли совпадающая строка */

     if (found == strstr(title, match)) {

      count++;

      strcpy(current_cd, title);

      strcpy(current_cat, catalog);

     }

    }

   }

  }

  fclose(titles_fp);

 }

 if (count != 1) {

  if (count == 0) {

   mvprintw(ERROR_LINE, 0, "Sorry, no matching CD found. ");

  }

  if (count > 1) {

   mvprintw(ERROR_LINE, 0,

    "Sorry, match is ambiguous: CDs found. ", count);

  }

  current_cd[0] = '\0';

  get_return();

 }

}

Хотя переменная catalog указывает на массив, больший чем current_cat, и могла бы переписать память, проверка в функции fgets препятствует этому.

3. Вам также нужно иметь возможность перечислить на экране дорожки выбранного компакт-диска. Для вложенных окон можно использовать директивы #define, применявшиеся в функции update_cd в предыдущем разделе.

void list_tracks() {

 FILE *tracks_fp;

 char entry[MAX_ENTRY];

 int cat_length;

 int lines_op = 0;

 WINDOW *track_pad_ptr;

 int tracks = 0;

 int key;

 int first_line = 0;

 if (current_cd[0] == '\0') {

  mvprintw(ERROR_LINE, 0, "You must select a CD first. ");

  get_return();

  return;

 }

 clear_all_screen();

 cat_length = strlen(current_cat);

 /* Сначала считает количество дорожек у текущего CD */

 tracks_fp = fopen(tracks_file, "r");

 if (!tracks_fp) return;

 while (fgets(entry, MAX_ENTRY, tracks_fp)) {

  if (strncmp(current_cat, entry, cat_length) == 0) tracks++;

 }

 fclose(tracks_fp);

 /* Создает новую панель, гарантируя, что даже при наличии одной

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

    prefresh() всегда будет допустим. */

 track_pad_ptr = newpad(tracks + 1 + ВОХЕD_LINES, BOXED_ROWS + 1);

 if (!track_pad_ptr) return;

 tracks_fp = fopen(tracks_file, "r");

 if (!tracks_fp) return;

 mvprintw(4, 0, "CD Track Listing\n");

 /* Записывает сведения о дорожке на панель */

 while (fgets(entry, MAX_ENTRY, tracks_fp)) {

  /* Сравнивает номер каталога и оставшийся вывод элемента */

  if (strncmp(current_cat, entry, cat_length) == 0) {

   mvwprintw(track_pad_ptr, lines_op++, 0, "%s", entry + cat_length + 1);

  }

 }

 fclose(tracks_fp);

 if (lines_op > BOXED_LINES) {

  mvprintw(MESSAGE_LINE, 0,

   "Cursor keys to scroll, RETURN or q to exit");

 } else {

  mvprintw(MESSAGE_LINE, 0, "RETURN or q to exit");

 }

 wrefresh(stdscr);

 keypad(stdscr, TRUE);

 cbreak();

 noecho();

 key = 0;

 while (key != "q" && key != KEY_ENTER && key != '\n') {

  if (key == KEY_UP) {

   if (first_line > 0) first_line--;

  }

  if (key == KEY_DOWN) {

   if (first_line + BOXED_LINES + 1 < tracks) first_line++;

  }

  /* Теперь рисует соответствующую часть панели на экране */

  prefresh(track_pad_ptr, first_line, 0, BOX_LINE_POS, BOX_ROW_POS,

   BOX_LINE_POS + BOXED_LINES, BOX_ROW_POS + BOXED_ROWS);

  key = getch();

 }

 delwin(track_pad_ptr);

 keypad(stdsсr, FALSE);

 nocbreak();

 echo();

}

4. В последних двух функциях вызывается функция get_return, которая приглашает к вводу и считывает символ возврата каретки, игнорируя другие символы.

void get_return() {

 int ch;

 mvprintw(23, 0, "is", " Press return ");

 refresh();

 while ((ch = getchar()) != '\n' && ch != EOF);

}

Если вы выполните эту программу, то увидите на экране нечто похожее на рис. 6.10.

Рис. 6.10 

Резюме 

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

Глава 7

Управление данными

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

Мы можем представить все эти темы как три способа управления данными:

□ управление динамической памятью: что делать и что Linux не разрешит делать;

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

□ база данных dbm: базовая, основанная не на запросах SQL библиотека базы данных, присутствующая в большинстве систем Linux.

Управляемая память

Во всех компьютерных системах память — дефицитный ресурс. Не важно, сколько памяти доступно, ее всегда не хватает. Кажется, совсем недавно считалось, что 256 Мбайт RAM вполне достаточно, а сейчас распространено мнение о том, что 2 Гбайт RAM — это обоснованное минимальное требование даже для настольных систем, а серверам полезно было бы иметь значительно больше.

У всех UNIX-подобных операционных систем, начиная с самых первых версий, был ясный подход к управлению памятью, который унаследовала ОС Linux, воплощающая стандарт X/Open. Приложениям в ОС Linux, за исключением нескольких специализированных встроенных приложений, никогда не разрешается напрямую обращаться к физической памяти. Приложению может казаться, что у него есть такая возможность, но самом деле это тщательно управляемая иллюзия. 

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

Простое выделение памяти

Вы можете выделить память с помощью вызова malloc из стандартной библиотеки С:

#include <stdlib.h>

void *malloc(size_t size);

Примечание

Имейте в виду, что ОС Linux (следующая требованиям стандарта X/Open) отличается от некоторых реализаций UNIX тем, что не требует включения специального заголовочного файла malloc.h. Кроме того, параметр size, задающий количество выделяемых байтов, — это не простой тип int, хотя обычно он задается типом беззнаковое целое (unsigned integer).

В большинстве систем Linux вы можете выделять большой объем памяти. Давайте начнем с очень простой программы из упражнения 7.1, которая, тем не менее, выигрывает соревнование со старыми программами ОС MS-DOS, поскольку они не могут обращаться к памяти за пределами базовой карты памяти ПК объемом 640 Кбайт.

Упражнение 7.1. Простое распределение памяти

Наберите следующую программу memory1.с:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#define A_MEGABYTE (1024 * 1024)

int main() {

 char *some_memory;

 int megabyte = A_MEGABYTE;

 int exit_code = EXIT_FAILURE;

 some_memory = (char*)malloc(megabyte);

 if (some_memory ! = NULL) {

  sprintf(some_memory, "Hello World\n");

  printf("%s", some_memory);

  exit_code = EXIT_SUCCESS;

 }

 exit(exit_code);

}

Когда вы выполните эту программу, то получите следующий вывод:

$ ./memory1

Hello World

Как это работает

Данная программа запрашивает с помощью библиотечного вызова malloc указатель на один мегабайт памяти. Вы проверяете, успешно ли завершился вызов malloc, и используете часть памяти, чтобы продемонстрировать ее наличие. Когда вы выполните программу, то увидите вывод фразы "Hello World", показывающий, что malloc действительно вернул мегабайт используемой памяти. Мы не проверяем наличие мегабайта целиком; мы приняли на веру программный код malloc!

Поскольку функция malloc возвращает указатель типа void*, вы преобразуете результат в нужный вам указатель типа char*. Эта функция возвращает память, выровненную так, что она может быть преобразована в указатель любого типа.

Простое основание — современные системы Linux применяют 32-разрядные целые и 32-разрядные указатели, что позволяет задавать до 4 Гбайт. Эта способность задавать адреса с помощью 32-разрядного указателя без необходимости применения регистров сегментов или других приемов, называется простой 32-разрядной моделью памяти. Эта модель также используется и в 32-разрядных версиях ОС Windows ХР и Vista. Тем не менее, никогда не следует рассчитывать на 32-разрядные целые, поскольку все возрастающее количество 64-разрядных версий Linux находится в употреблении.

Выделение огромных объемов памяти

Теперь, когда вы увидели, что ОС Linux преодолевает ограничения модели памяти ОС MS-DOS, давайте усложним ей задачу. Приведенная в упражнении 7.2 программа запрашивает выделение объема памяти, большего, чем физически есть в машине, поэтому можно предположить, что функция malloc начнет давать сбои при приближении к максимальному объему физической памяти, поскольку ядру и всем остальным выполняющимся процессам также нужна память.

Упражнение 7.2. Запрос на всю физическую память

С помощью программы memory2.с мы собираемся запросить больше памяти, чем физически есть в машине. Вам нужно откорректировать определение PHY_MEM_MEGS в соответствии с физическими ресурсами вашего компьютера.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#define A_MEGABYTE (1024 * 1024)

#define PHY_MEM_MEGS 1024 /* Откорректируйте это число

                             должным образом */

int main() {

 char *some_memory;

 size_t size_to_allocate = A_MEGABYTE;

 int megs_obtained = 0;

 while (megs_obtained < (PHY_MEM_MEGS * 2)) {

  some_memory = (char *)malloc(size_to_allocate);

  if (some_memory != NULL) {

   megs_obtained++;

   sprintf(somememory, "Hello World");

   printf("%s — now allocated %d Megabytes\n", some_memory, megs_obtained);

  } else {

   exit(EXIT_FAILURE);

  }

 }

 exit(EXIT_SUCCESS);

}

Далее приведен немного сокращенный вывод:

$ ./memory3

Hello World — now allocated 1 Megabytes

Hello World — now allocated 2 Megabytes

...

Hello World — now allocated 2047 Megabytes

Hello World — now allocated 2048 Megabytes

Как это работает

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

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

Продолжим исследование и посмотрим, сколько памяти мы сможем выделить на этой машине с помощью программы memory3.c (упражнение 7.3). Поскольку уже понятно, что система Linux способна очень умно обходиться с запросами памяти, мы каждый раз будем выделять память по 1 Кбайт и записывать данные в каждый полученный нами блок.

Упражнение 7.3. Доступная память

Далее приведена программа memory3.c. По своей истинной природе она крайне недружественная по отношению к пользователю и может очень серьезно повлиять на многопользовательскую машину. Если вас беспокоит подобный риск, лучше совсем не запускать ее; если вы окажитесь от выполнения этой программы, усвоению материала это не повредит.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#define ONE_K (1024)

int main() {

 char *some_memory;

 int size_to_allocate = ONE_K;

 int megs_obtained = 0;

 int ks_obtained = 0;

 while (1) {

  for (ks_obtained = 0; ks_obtained < 1024; ks_obtained++) {

   some_memory = (char *)malloc(size_to_allocate);

   if (some_memory == NULL) exit(EXIT_FAILURE);

   sprintf(some_memory, "Hello World");

  }

  megs_obtained++;

  printf("Now allocated %d Megabytes\n", megs_obtained);

 }

 exit(EXIT_SUCCESS);

}

На этот раз вывод, также сокращенный, выглядит следующим образом:

$ ./memory3

Now allocated 1 Megabytes

...

Now allocated 1535 Megabytes

Now allocated 1536 Megabytes

Out of Memory: Killed process 2365

Killed

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

Как это работает

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

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

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

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

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

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

Примечание

Это поведение, сопровождающееся уничтожением процесса, отличается от поведения более старых версий Linux и множества других вариантов UNIX, в которых просто аварийно завершалась функция malloc. Называется оно уничтожением из-за нехватки памяти (out of memory (OOM) killer), и хотя может показаться чересчур радикальным, на самом деле служит разумным компромиссом между возможностью быстрого и эффективного выделения памяти процессам и необходимостью собственной защиты ядра от полного исчерпания ресурсов, что является серьезной проблемой.

Но что это означает для прикладного программиста? В основном благую весть. Система Linux очень умело управляет памятью и позволяет приложениям использовать большие области памяти и даже очень большие единые блоки памяти. Но вы должны помнить о том, что выделение двух блоков памяти в результате не приведет к формированию одного непрерывно адресуемого блока памяти. Вы получите то, что просили: два отдельных блока памяти.

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

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

Неправильное обращение к памяти

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

Упражнение 7.4. Неправильное обращение к вашей памяти

#include <stdlib.h>

#define ONE_K (1024)

int main() {

 char *some_memory;

 char *scan_ptr;

 some_memory = (char *)malloc(ONE_K);

 if (some_memory == NULL) exit(EXIT_FAILURE);

 scan_ptr = some_memory;

 while (1) {

  *scan_ptr = '\0';

  scan_ptr++;

 }

 exit(EXIT_SUCCESS);

}

Вывод прост:

$ ./memory4

Segmentation fault

Как это работает

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

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

Указатель null

Современные системы Linux, в отличие от ОС MS-DOS, но подобно новейшим вариантам ОС Windows, надежно защищены от записи или чтения по адресу, на который ссылается пустой указатель (null), хотя реальное поведение системы зависит от конкретной реализации.

Выполните упражнение 7.5.

Упражнение 7.5. Обращение по указателю null

Давайте выясним, что произойдет, когда мы попытаемся обратиться к памяти по пустому или null-указателю в программе memory5a.c.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

int main() {

 char *some_memory = (char*)0;

 printf("A read from null %s\n", some_memory);

 sprintf(some_memory, "A write to null\n");

 exit(EXIT_SUCCESS);

}

Будет получен следующий вывод:

$ ./memory5a

A read from null (null)

Segmentation fault

Как это работает

Первая функция printf пытается вывести строку, полученную от указателя null; далее sprintf пытается записать по указателю null. В данном случае Linux (под видом библиотеки GNU С) простила чтение и просто предоставила "магическую" строку, содержащую символы (null)\0. Система не столь терпима в случае записи и просто завершила программу. Такое поведение порой полезно при выявлении программных ошибок.

Если вы повторите попытку, но не будете использовать библиотеку GNU С, вы обнаружите, что безадресное чтение не разрешено. Далее приведена программа memory5b.c:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

int main() {

 char z = *(const char *)0;

 printf("I read from location zero\n");

 exit(EXIT_SUCCESS);

}

Вы получите следующий результат:

$ ./memory5b

Segmentation fault

В этот раз вы пытаетесь прочесть непосредственно из нулевого адреса. Между вами и ядром теперь нет GNU-библиотеки libc, и программа прекращает выполнение. Имейте в виду, что некоторые системы UNIX разрешают читать из нулевого адреса, ОС Linux этого не допускает.

Освобождение памяти

До сих пор мы выделяли память и затем надеялись на то, что по завершении программы использованная нами память не будет потеряна, К счастью, система управления памятью в ОС Linux вполне способна с высокой степенью надежности гарантировать возврат памяти в систему по завершении программы. Но большинство программ просто не хотят распределять память, используют ее очень короткий промежуток времени и затем завершаются. Гораздо более распространено динамическое использование памяти по мере необходимости.

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

#include <stdlib.h>

void free(void *ptr_to_memory);

Вызов free следует выполнять только с указателем на память, выделенную с помощью вызова malloc, calloc или realloc. Очень скоро вы встретитесь с функциями calloc и realloc. А сейчас выполните упражнение 7.6.

Упражнение 7.6. Освобождение памяти

Эта программа называется memory6.c.

#include <stdlib.h>

#include <stdio.h>

#define ONE_K (1024)

int main() {

 char *some_memory;

 int exit code = EXIT_FAILURE;

 some_memory = (char*)malloc(ONE_K);

 if (some_memory != NULL) {

  free(some_memory);

  printf("Memory allocated and freed again\n");

  exit_code = EXIT_SUCCESS;

 }

 exit(exit_code);

}

Вывод программы следующий:

$ ./memory6

Memory allocated and freed again

Как это работает

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

Примечание

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

Другие функции распределения памяти

Две другие функции распределения или выделения памяти calloc и realloc применяются не так часто, как malloc и free.

Далее приведены их прототипы:

#include <stdlib.h>

void *calloc(size_t number_of_elements, size_t element_size);

void *realloc(void *existing_memozy, size_t new_size);

Несмотря на то, что функция calloc выделяет память, которую можно освободить с помощью функции free, ее параметры несколько отличаются от параметров функции malloc: она выделяет память для массива структур и требует задания количества элементов и размера каждого элемента массива как параметров. Выделенная память заполняется нулями; и если функция calloc завершается успешно, возвращается указатель на первый элемент. Как и в случае функции malloc, последовательные вызовы не гарантируют возврата непрерывной области памяти, поэтому вы не можете увеличить длину массива, созданного функцией calloc, просто повторным вызовом этой функции и рассчитывать на то, что второй вызов вернет память, добавленную в конец блока памяти, полученного после первого вызова функции.

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

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

my_ptr = malloc(BLOCK_SIZE);

...

my_ptr = realloc(my_ptr, BLOCK_SIZE * 10);

Если realloc завершится аварийно, она вернет пустой указатель; переменная my_ptr будет указывать в никуда и к первоначальной области памяти, выделенной функцией malloc, больше нельзя будет обратиться с помощью указателя my_ptr. Следовательно, было бы полезно сначала запросить новый блок памяти с помощью malloc, а затем скопировать данные из старого блока памяти в новый блок с помощью функции memcpy и освободить старый блок памяти вызовом free. При возникновении ошибки это позволит приложению сохранить доступ к данным, хранящимся в первоначальном блоке памяти, возможно, на время организации корректного завершения программы.

Блокировка файлов

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

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

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

Создание файлов с блокировкой

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

Как правило, эти заблокированные файлы находятся в специальном месте и имеют имена, связанные с управляемыми ими ресурсами. Например, когда используется модем, система Linux создает файл с блокировкой, часто применяя каталог в каталоге /var/spool.

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

Для создания файла с блокировкой (упражнение 7.7) можно использовать системный вызов open, определенный в файле fcntl.h (уже встречавшемся в предыдущих главах) и содержащий набор флагов O_CREAT и O_EXCL. Этот способ позволяет проверить, не существует ли уже такой файл, и затем создать его за одну элементарную неделимую операцию.

Упражнение 7.7. Создание файла с блокировкой

В программе lock1.c вы сможете увидеть файл с блокировкой в действии.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <errno.h>

int main() {

 int file_desc;

 int save_errno;

 file_desc = open("/tmp/LCK.test", O_RDWR | O_CREAT | O_EXCL, 0444);

 if (file_desc == -1) {

  save errno = errno;

  printf("Open failed with error %d\n", save_errno);

 } else {

  printf("Open succeeded\n");

 }

 exit(EXIT_SUCCESS);

} 

Выполнив программу первый раз, вы получите следующий вывод:

./lock1

Open succeeded

Но при повторной попытке вы получите результат, приведенный далее:

$ ./lock1

Open failed with error 17

Как это работает

Для создания файла с именем /tmp/LCK.test программа выполняет вызов, использующий флаги O_CREAT и O_EXCL. Во время первого выполнения программы файл не существует, поэтому вызов open завершается успешно. Последующие запуски программы завершаются аварийно, потому что файл уже существует. Для успешного выполнения этой программы в дальнейшем вы должны вручную удалить файл с блокировкой.

В системах Linux, ошибка 17 соответствует константе EEXIST, указывающей на то, что файл уже существует. Номера ошибок определены в файле errno.h или, скорее, в файлах, включаемых этим файлом. В данном случае определение в действительности, находящееся в /usr/include/asm-generic/errno-base.h, гласит

#define EEXIST 17 /* File exists */

Это ошибка, соответствующая аварийному завершению вызова open(O_CREAT | O_EXCL).

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

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

Упражнение 7.8. Совместная блокировка файлов

1. Далее приведен исходный код тестовой программы lock2.с.

#include <unistd.h>

#include <stdlib.h> 

#include <stdio.h>

#include <fcntl.h>

#include <errno.h>

const char *lock_file = "/tmp/LCK.test2";

int main() {

 int file_desc;

 int tries = 10;

 while (--tries) {

  file_desc = open(lock_file, O_RDWR | O_CREAT | O_EXCL, 0444);

  if (file_desc == -1) {

   printf("%d - Lock already present\n", getpid());

   sleep(3);

  } else {

2. Далее следует критическая секция:

   printf("%d — I have exclusive access\n", getpid());

   sleep(1);

   (void)close(file_desc);

   (void)unlink(lockfile);

3. В этом месте она заканчивается:

   sleep(2);

  }

 }

 exit(EXIT_SUCCESS);

}

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

$ rm -f /tmp/LCK.test2

Затем с помощью приведенной далее команды запустите две копии программы:

$ ./lock2 & ./lock2

Она запускает одну копию программы в фоновом режиме, а вторую — как основную программу. Далее приведен вывод:

1284 — I have exclusive access

1283 — Lock already present

1283 — I have exclusive access

1284 — Lock already present

1284 — I have exclusive access

1283 — Lock already present

1283 — I have exclusive access

1284 — Lock already present

1284 — I have exclusive access

1283 — Lock already present

1283 — I have exclusive access

1284 — Lock already present

1284 — I have exclusive access

1283 — Lock already present

1283 — I have exclusive access

1284 — Lock already present

1284 — I have exclusive access

1283 — Lock already present

1283 — I have exclusive access

1284 — Lock already present

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

Как это работает

Для демонстрации вы 10 раз выполняете в программе цикл с помощью оператора while. Затем программа пытается получить доступ к дефицитному ресурсу, создав уникальный файл с блокировкой /tmp/LCK.test2. Если эта попытка терпит неудачу из-за того, что файл уже существует, программа ждет короткий промежуток времени и затем снова пытается создать файл. Если ей это удается, она получает доступ к ресурсу и в части программы, помеченной как "критическая секция", выполняет любую обработку, требующую исключительных прав доступа.

Поскольку это всего лишь пример, вы ждете очень короткий промежуток времени. Когда программа завершает использование ресурса, она снимает блокировку, удаляя файл с блокировкой. Далее она может выполнить другую обработку (в данном случае это просто функция sleep) прежде, чем попытаться возобновить блокировку. Файлы с блокировкой действуют как двоичный семафор, давая программе ответ "да" или "нет" на вопрос: "Могу ли я использовать ресурс?". В главе 14 вы узнаете больше о семафорах.

Примечание

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

Блокировка участков файла

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

Урегулировать эту ситуацию можно, блокируя участки файла. При этом конкретная часть файла блокируется, но другие программы могут иметь доступ к другим участкам файла. Это называется блокировкой сегментов или участков файла. У системы Linux есть (как минимум) два способа сделать это: с помощью системного вызова fcntl или системного вызова lockf. Мы рассмотрим интерфейс fcntl, поскольку он наиболее часто применяется. Интерфейс lockf в основном аналогичен, и в ОС Linux он используется как альтернативный интерфейсу fcntl. Однако блокирующие механизмы fcntl и lockf не работают вместе: у них разные низкоуровневые реализации. Поэтому никогда не следует смешивать вызовы этих двух типов; выберите один или другой.

Вы встречали вызов fcntl в главе 3. У него следующее определение:

#include <fcntl.h>

int fcntl(int fildes, int command, ...);

Системный вызов fcntl оперирует открытыми дескрипторами файлов и, в зависимости от параметра command, может выполнять разные задачи. Для блокировки файлов интересны три приведенные далее возможные значения параметра command:

F_GETLK;

F_SETLK;

F_SETLKW.

Когда вы используете эти варианты, третий аргумент в вызове должен быть указателем на структуру struct flock, поэтому на самом деле прототип вызова выглядит следующим образом:

int fcntl(int fildes, int command, struct flock *flock_structure);

Структура flock (он англ. file lock) зависит от конкретной реализации, но, как минимум, она будет содержать следующие элементы:

short l_type;

short l_whence;

off_t l_start;

off_t l_len;

pid_t l_pid.

Элемент l_type принимает одно из нескольких значений (табл. 7.1), определенных в файле fcntl.h.

Таблица 7.1.

Значение Описание
F_RDLCK Разделяемая или совместная блокировка (блокировка на чтение). У разных процессов может быть разделяемая блокировка одних и тех же (или перекрывающихся) участков файла. Если у какого-либо процесса есть разделяемая блокировка, ни один процесс не сможет установить исключительную блокировку этого участка. Для получения совместной блокировки файл должен быть открыт с правом на чтение или на чтение/запись
F_UNLCK Разблокировать. Применяется для снятия блокировок
F_WRLCK Исключительная блокировка (или блокировка на запись). Только один процесс может установить исключительную блокировку на любой конкретный участок файла. После того как процесс установил такую блокировку, никакой другой процесс не сможет установить блокировку любого типа на этот участок файла. Для установки исключительной блокировки файл должен быть открыт с правом на запись или на чтение/запись

Элементы l_whence, l_start и l_len определяют участок файла, непрерывную область в байтах. Элемент l_whence должен задаваться одним из следующих значений: SEEK_SET, SEEK_CUR, SEEK_END (из файла unistd.h). Они соответствуют началу, текущей позиции или концу файла соответственно. Элемент l_whence задает смещение для первого байта участка файла, определенного элементом l_start. Обычно оно задается константой SEEK_SET, поэтому l_start отсчитывается от начала файла. Параметр l_len содержит количество байтов в участке файла.

Параметр l_pid применяется для указания процесса, установившего блокировку; см. следующее далее описание значения F_GETLK параметра command.

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

Значение F_GETLK параметра command

Первое значение параметра command — F_GETLK. Эта команда получает информацию о файле, который открыт fildes (первый параметр в вызове). Она не пытается блокировать файл. В процессе вызова передаются сведения о типе блокировки, которую хотелось бы установить, и вызов fcntl с командой F_GETLK возвращает любую информацию, которая могла бы помешать установке блокировки.

Значения, используемые в структуре flock, приведены в табл. 7.2.

Таблица 7.2

Значение Описание
l_type Или F_RDLCK для разделяемой (только чтение) блокировки, или F_WRLCK для исключительной (на запись) блокировки
l_whence Одно из значений: SEEK_SET, SEEK_CUR или SEEK_END LCK
l_start Начальный байт интересующего вас участка файла
l_len Количество байтов в интересующем вас участке файла
l_pid Идентификатор процесса, удерживающего блокировку

Процесс может применять вызов с командой F_GETLK для определения текущего состояния блокировки участка файла. Он должен настроить структуру flock, указав тип требуемой блокировки и определив интересующую его область файла. Вызов fcntl возвращает в случае успешного завершения значение, отличное от -1. Если у файла уже есть блокировки, препятствующие установке требуемой блокировки, структура flock обновляется соответствующими данными. Если блокировке ничто не мешает, структура flock не изменяется. Если вызов с командой F_GETLK не может получить информацию, он возвращает -1 для обозначения аварийного завершения.

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

Значение F_SETLK параметра command

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

Таблица 7.3

Значение Описание
l_type Одно из следующих: • F_RDLCK — для разделяемой или допускающей только чтение блокировки; • F_WRLCK — для исключительной или блокировки записи; • F_UNLCK — для разблокирования участка
l_pid Не используется

Как и в случае F_GETLK, блокируемый участок определяется значениями элементов l_start, l_whence и l_len структуры flock. Если блокировка установлена, вызов fcntl вернет значение, отличное от -1, при аварийном завершении возвращается -1. Вызов завершается немедленно.

Значение F_SETLKW параметра command

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

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

Применение вызовов read и write при наличии блокировки

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

Для того чтобы понять, в чем тут проблема, рассмотрим две программы, которые хотят обновить один и тот же файл. Предположим, что файл содержит 200 байтов данных, все нули. Первая программа начинает работу и устанавливает блокировку на запись для первых 100 байтов файла. Затем она применяет функцию fread для считывания этих 100 байтов. Однако, как было показано в одной из предшествующих глав, fread будет каждый раз считывать больше, до BUFSIZ байтов, поэтому она на самом деле считает в память целиком весь файл, но программе вернет только первые 100 байтов.

Затем стартует вторая программа. Она устанавливает блокировку write на вторые 100 байтов файла. Это действие завершится успешно, поскольку первая программа заблокировала только первые 100 байтов. Вторая программа записывает двойки в байты с 100-го по 199-й, закрывает файл, снимает блокировку и завершается. В это время первая программа блокирует вторые 100 байтов файла и вызывает функцию fread для их считывания. Поскольку эти данные были уже занесены библиотекой в буфер, программа увидит 100 байтов нулей, а не 100 двоек, которые на самом деле хранятся в файле на жестком диске. Подобной проблемы не возникает, если вы применяете вызовы read и write.

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

Упражнение 7.9. Блокировка файла с помощью вызова fcntl

Давайте рассмотрим пример работы блокировки файла в программе lock3.с. Для опробования блокировки вам понадобятся две программы: одна для установки блокировки и другая для ее тестирования. Первая программа выполняет блокировку.

1. Начните с файлов include и объявлений переменных:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

int main() {

 int file desc;

 int byte_count;

 char *byte_to_write = "A";

 struct flock region_1;

 struct flock region_2;

 int res;

2. Откройте файловый дескриптор:

 file_desc = open(test_file, O_RDWR | O_CREAT, 0666);

 if (!file_desc) {

  fprintf(stderr, "Unable to open %s for read/write\n", test_file);

  exit(EXIT_FAILURE);

 }

3. Поместите данные в файл:

 for (byte_count = 0; byte_count < 100; byte_count++) {

  (void)write(file_desc, byte_to_write, 1);

 }

4. Задайте разделяемую блокировку для участка region 1 с 10-го байта по 30-й:

 region_1.l_type = F_RDLCK;

 region_1.l_whence = SEEK_SET;

 region_1.l_start = 10;

 region_1.l_len = 20;

5. Задайте исключительную блокировку для участка region_2 с 40-го байта по 50-й:

 region_2.l_type = F_WRLCK;

 region_2.l_whence = SEEK_SET;

 region_2.l_start = 40;

 region_2.l_len = 10;

6. Теперь заблокируйте файл:

 printf("Process %d locking file\n", getpid());

 res = fcntl(file_desc, F_SETLK, &region_1);

 if (res == -1) fprintf(stderr, "Failed to lock region 1\n");

 res = fcntl(file_desc, F_SETLK, &region_2);

 if (res = fprintf(stderr, "Failed to lock region 2\n");

7. Подождите какое-то время:

 sleep(60);

 printf ("Process %d closing file\n", getpid());

 close(file_desc);

 exit(EXIT_SUCCESS);

}

Как это работает

Сначала программа создает файл, открывает его для чтения и записи и затем заполняет файл данными. Далее задаются два участка: первый с 10-го по 30-й байт для разделяемой блокировки и второй с 40-го по 50-й байт для исключительной блокировки. Затем программа выполняет вызов fcntl для установки блокировок на два участка файла и ждет в течение минуты, прежде чем закрыть файл и завершить работу.

На рис. 7.1 показан этот сценарий с блокировками в тот момент, когда программа переходит к ожиданию.

Рис. 7.1

Сама по себе эта программа не очень полезна. Вам нужна вторая программа lock4.c для тестирования блокировок (упражнение 7.10).

Упражнение 7.10. Тестирование блокировок файла

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

1. Как обычно, начнем с заголовочных файлов и объявлений:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

#define SIZE_TO_TRY 5

void show_lock_info(struct flock *to_show);

int main() {

 int file_desc;

 int res;

 struct flock region_to_test;

 int start_byte;

2. Откройте дескриптор файла:

 file_desc = open(test_file, O_RDWR | O_CREAT, 0666);

 if (!file_desc) {

  fprintf(stderr, "Unable to open %s for read/write", test_file);

  exit(EXIT_FAILURE);

 }

 for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {

3. Задайте участок файла, который хотите проверить:

  region_to_test.l_type = F_WRLCK;

  region_to_test.l_whence = SEEK_SET;

  region_to_test.lstart = start_byte;

  region_to_test.l_len = SIZE_TO_TRY;

  region_to_test.l_pid = -1;

  printf("Testing F_WRLCK on region from %d to %d\n", start_byte, start_byte + SIZE_TO_TRY);

4. Теперь проверьте блокировку файла:

  res = fcntl(file_desc, F_GETLK, &region_to_test);

  if (res == -1) {

   fprintf(stderr, "F_GETLK failed\n");

   exit(EXIT_FAILURE);

  }

  if (region_to_test.l_pid != -1) {

   printf("Lock would fail. F_GETLK returned:\n");

   showlockinfo(&region_to_test);

  } else {

   printf("F_WRLCK - Lock would succeed\n");

  }

5. Далее повторите тест с разделяемой блокировкой (на чтение). Снова задайте участок файла, который хотите проверить:

  region_to_test.l_type = F_RDLCK;

  region_to_test.l_whence = SEEK_SET;

  region_to_test.l_start = start_byte;

  region_to_test.l_len = SIZE_TO_TRY;

  region_to_test.l_pid = -1;

  printf("Testing F_RDLCK on region from %d to %d\n", start_byte, start_byte + SIZE_TO_TRY);

6. Еще раз проверьте блокировку файла:

  res = fcntl(file_desc, F_GETLK, &region_to_test);

  if (res == -1) {

   fprintf(stderr, "F_GETLK failed\n");

   exit(EXIT_FAILURE);

  }

  if (region_to_test.l_pid != -1) {

   printf("Lock would fail. F_GETLK returned:\n");

   show_lock_info(&region_to_test);

  } else {

   printf("F_RDLCK — Lock would succeed\n");

  }

 }

 close(file_desc);

 exit(EXIT_SUCCESS);

}

void show_lock_info(struct flock *to_show) {

 printf("\tl_type %d, ", to_show->l_type);

 printf("l_whence %d, ", to_show->l_whence);

 printf("l_start %d, (int)to_show->l_start);

 printf("l_len %d, ", (int)to_show->l_len);

 printf("l_pid %d\n", to_show->l_pid);

}

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

$ ./lock3 &

$ process 1534 locking file

На экране появится приглашение для ввода команд, поскольку lock3 выполняется в фоновом режиме. Далее сразу же запустите программу lock4 с помощью следующей команды:

$ ./lock4

Вы получите вывод, приведенный далее с некоторыми пропусками для краткости:

Testing F_WRLCK on region from 0 to 5

F_WRLCK — Lock would succeed

Testing F_RDLCK on region from 0 to 5

F_RDLCK - Lock would succeed

...

Testing F_WRLCK on region from 10 to 15

Lock would fail. F_GETLK returned:

l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534

Testing F_RDLCK on region from 10 to 15

F_RDLCK — Lock would succeed

Testing F_WRLCK on region from 15 to 20

Lock would fail. F_GETLK returned:

l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534

Testing F_RDLCK on region from 15 to 20

F_RDLCK — Lock would succeed

...

Testing F_WRLCK on region from 25 to 30

Lock would fail. F_GETLK returned:

l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534

Testing F_RDLCK on region from 25 to 30

F_RDLCK — Lock would succeed

...

Testing F_WRLCK on region from 40 to 45

Lock would fail. F_GETLK returned:

l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534

Testing F_RDLCK on region from 40 to 45

Lock would fail. F_GETLK returned:

l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534

...

Testing F_RDLCK on region from 95 to 100

F_RDLCK - Lock would succeed

Как это работает

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

Для того чтобы понять вывод, следует заглянуть в заголовочный файл fcntl.h (обычно /usr/include/fcntl.h) и увидеть, что поле l_type, равное 1, вытекает из определения F_WRLCK как 1, а равное 0 из определения F_RDLCK как 0. Таким образом, поле l_type, равное 1, говорит о том, что блокировка не будет установлена, поскольку существует блокировка на запись, а поле l_type, равное 0, свидетельствует о существовании блокировки на чтение. Для тех участков файла, которые не заблокировала программа lock3, могут быть установлены и разделяемая, и исключительная блокировки.

Для байтов с 10-го по 30-й возможна установка разделяемой блокировки, поскольку блокировка, установленная программой lock3, не исключительная, а разделяемая. Для участка с 40-го по 50-й байт нельзя установить оба типа блокировки, поскольку lock3 задала исключительную (F_WRLCK) блокировку для этого участка.

После завершения программы lock4 необходимо немного подождать, чтобы программа lock3 завершила вызов sleep и закончила выполнение.

Конкурирующие блокировки

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

Упражнение 7.11. Конкурирующие блокировки

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

После директив #include и объявлений откройте дескриптор файла.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

int main() {

 int file_desc;

 struct flock region_to_lock;

 int res;

 file_desc = open(test_file, O_RDWR | O_CREAT, 0666);

 if (!file_desc) {

  fprintf(stderr, "Unable to open %s for read/write\n", test_file);

  exit(EXIT_FAILURE);

 }

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

 region_to_lock.l_type = F_RDLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.l_start = 10;

 region_to_lock.l_len = 5;

 printf("Process %d, trying F_RDLCK, region %d to %d\n", getpid(), (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(file_desc, F_SETLK, &region_to_lock);

 if (res == -1) {

  printf("Process %d - failed to lock region\n", getpid());

 } else {

  printf("Process %d — obtained lock region\n", getpid());

 }

 region_to_lock.l_type = F_UNLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.l_start = 10;

 region_to_lock.l_len = 5;

 printf("Process %d, trying F_UNLCK, region %d to %d\n", getpid(), (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(file_desc, F_SETLK, &region_to_lock);

 if (res == -1) {

  printf("Process %d — failed to unlock region\n", getpid());

 } else {

  printf("Process %d — unlocked region\n", getpid());

 }

 region_to_lock.l_type = F_UNLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.l_start = 0;

 region_to_lock.l_len = 50;

 printf("Process %d, trying F_UNLCK, region %d to %d\n", getpid()", (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(file_desc, F_SETLK, &region_to_lock);

 if (res == -1) {

  printf("Process %d — failed to unlock region\n", getpid());

 } else {

  printf("Process %d — unlocked region\n", getpid());

 }

 region_to_lock.l_type = F_WRLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.lstart = 16;

 region_to_lock.l_len = 5;

 printf("Process %d, trying F_WRLCK, region %d to %d\n", getpid(), (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(file_desc, F_SETLK, &region_to_lock);

 if (res == -1) {

  printf("Process %d — failed to lock region\n", getpid());

 } else {

  printf("Process %d — obtained lock on region\n", getpid());

 }

 region_to_lock.l_type = F_RDLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.l_start = 40;

 region_to_lock.l_len = 10;

 printf("Process %d, trying F_RDLCK, region %d to %d\n", getpid(), (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(filedesc, F_SETLK, &region_to_lock);

 if (res == -1) {

  printf("Process %d — failed to lock region\n", getpid());

 } else {

  printf("Process %d — obtained lock on region\n", getpid());

 }

 region_to_lock.l_type = F_WRLCK;

 region_to_lock.l_whence = SEEK_SET;

 region_to_lock.l_start = 16;

 region_to_lock. l_len = 5;

 printf("Process %d, trying F_WRLCK with wait, region %d to %d\n", getpid(), (int)region_to_lock.l_start,

  (int)(region_to_lock.l_start + region_to_lock.l_len));

 res = fcntl(file_desc, F_SETLKW, &region_to_lock);

 if (res == -1) {

  printf("Process %d — failed to lock region\n", getpid());

 } else {

  printf("Process %d — obtained lock, on region\n", getpid());

 }

printf ("Process %d ending\n", getpid());

 close(file_desc);

 exit(EXIT_SUCCESS);

}

Если вы сначала запустите программу lock3 в фоновом режиме, далее сразу запускайте новую программу:

$ ./lock3 &

$ process 227 locking file

$ ./lock5

Вы получите следующий вывод:

Process 227 locking file

Process 228, trying F_RDLCK, region 10 to 15

Process 228 — obtained lock on region

Process 228, trying F_UNLCK, region 10 to 15

Process 228 — unlocked region

Process 228, trying F_UNLCK, region 0 to 50

Process 228 — unlocked region

Process 228, trying F_WRLCK, region 16 to 21

Process 228 — failed to lock on region

Process 228, trying F_RDLCK, region 4 0 to 50

Process 228 - failed to lock on region

Process 228, trying F_WRLCK with wait, region 16 to 21

Process 227 closing file

Process 228 — obtained lock on region

Process 228 ending

Как это работает

Сначала программа пытается заблокировать участок с 10-го по 15-й байты с помощью разделяемой блокировки. Эта область уже заблокирована блокировкой того же типа, но одновременные разделяемые блокировки допустимы, и установка блокировки завершается успешно.

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

Далее программа пытается заблокировать участок с 16-то по 21-й байты исключительной блокировкой. Эта область уже заблокирована разделяемой блокировкой, поэтому новая попытка блокировки завершается аварийно, т.к. не может быть создана исключительная блокировка. 

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

В заключение программа опять пытается получить исключительную блокировку для участка с 16-го по 21-й байты, но в этот раз она применяет команду F_SETLKW, позволяющую ждать до тех пор, пока блокировка не будет установлена. В выводе наступает долгая пауза, длящаяся, пока программа lock3, заблокировавшая этот участок, завершает вызов sleep и закрывает файл, тем самым снимая все установленные блокировки. Программа lock5 возобновляет выполнение, успешно блокирует участок файла и затем тоже завершается.

Другие команды блокировок

Есть второй метод блокировки файлов — функция lockf. Она тоже действует, используя дескрипторы файлов.

У функции следующий прототип:

#include <unistd.h>

int lockf(int fildes, int function, off_t size_to_lock);

Параметр function может принимать следующие значения:

□ F_ULOCK — разблокировать;

□ F_LOCK — заблокировать монопольно;

□ F_TLOCK — проверить и заблокировать монопольно;

□ F_TEST — проверить наличие блокировок других процессов.

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

Как и в случае вызова fcntl, все блокировки только рекомендательные; они на самом деле не могут помешать чтению из файла или записи в файл. За проверку имеющихся блокировок отвечают программы. Эффект от смешивания блокировок с помощью fcntl и блокировок с помощью lockf непредсказуем, поэтому вам следует решить, какой способ выбрать, и строго его придерживаться.

Взаимоблокировки

Обсуждение блокировок не было бы законченным без упоминания об опасности взаимоблокировок или тупиков. Предположим, что две программы хотят обновить один и тот же файл. Им обеим нужно обновить байт 1 и байт 2 одновременно. Программа А выбирает первым обновление байта 2, затем байта 1. Программа В пытается обновить сначала байт 1, затем байт 2.

Обе программы стартуют одновременно. Программа А блокирует байт 2, а программа В — байт 1. Программа А пытается установить блокировку для байта 1. Поскольку он уже заблокирован программой В, программа А ждет. Программа В пытается заблокировать байт 2. Поскольку он уже заблокирован программой А, программа В тоже ждет.

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

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

Примечание

В этой книге из-за ограниченности объема у нас нет возможности рассматривать трудности действующих одновременно программ. Если вы хотите почитать побольше об этом, попробуйте найти книгу: Ben-Ari М. Principles of Concurrent and Distributed Programming. — Prentice Hall, 1990 (Бен-Ари M. Принципы параллельного и распределенного программирования).

Базы данных

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

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

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

База данных dbm

Все версии Linux и большая часть вариантов систем UNIX поставляются с базовым, но очень эффективным набором подпрограмм для хранения данных, называемым базой данных dbm. База данных dbm отлично подходит для хранения индексированных данных, которые относительно статичны. Некоторые консерваторы в области баз данных могут возразить, что dbm — вовсе не база данных, а просто система хранения индексных файлов. Стандарт X/Open, тем не менее, называет dbm базой данных, поэтому в книге мы будем продолжать называть ее так же.

Введение в базу данных dbm

Несмотря на взлет свободно распространяемых реляционных баз данных, таких как MySQL и PostgreSQL, база данных dbm продолжает играть важную роль в системе Linux. Дистрибутивы, использующие RPM, например, Red Hat и SUSE, применяют dbm как внутреннее хранилище для данных устанавливаемых пакетов. Реализация LDAP с открытым кодом, Open LDAP (Lightweight Directory Access Protocol, облегченный протокол доступа к каталогу), также может применять dbm как механизм хранения. Преимущества dbm по сравнению с более сложными базами данных, такими как MySQL, в ее "легковесности" и возможности более простого встраивания в распределенный двоичный код (distributed binary), поскольку не требуется установка отдельного сервера базы данных. Во время написания книги программы Sendmail и Apache использовали dbm.

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

В данный момент мы сталкиваемся с небольшой проблемой: в течение многих лет было сформировано несколько версий базы данных dbm с разными API и средствами. Существует исходный набор dbm, "новый" набор dbm, называемый ndbm, и реализация проекта GNU gdbm. Реализация GNU может эмулировать интерфейсы более старой версии dbm и версии ndbm, но ее собственный интерфейс существенно отличается от других реализаций. Различные дистрибутивы Linux поставляются с библиотеками разных версий dbm, но самый популярный вариант — поставка с библиотекой gdbm и установка ее с возможностью эмуляции интерфейсов двух других типов.

В книге мы собираемся сосредоточиться на интерфейсе ndbm, поскольку он стандартизован X/OPEN и его применять легче, чем непосредственно интерфейс реализации gdbm.

Получение dbm

Самые широко распространенные дистрибутивы Linux приходят с уже установленной версией gdbm, хотя в некоторых из них вам придется применить соответствующий диспетчер пакетов (package manager) для установки нужных библиотек разработки. Например, в дистрибутиве Ubuntu вам может понадобиться диспетчер пакетов Synaptic для установки пакета libgdbm-dev, если он не установлен по умолчанию.

Если вы хотите просмотреть исходный код или используете дистрибутив, в который не включен встроенный пакет разработки, реализацию GNU можно найти по адресу www.gnu.org/software/gdbm/gdbm.html.

Устранение неполадок и повторная установка dbm

Эта глава написана в расчете на то, что у вас установлена реализация GNU gdbm, укомплектованная библиотеками совместимости с ndbm. Это обычный вариант для дистрибутивов Linux, однако, как упоминалось ранее, возможно, вам придется явно устанавливать пакет библиотеки разработки для того, чтобы компилировать файлы с использованием подпрограмм ndbm.

К сожалению, требуемые библиотеки директив include и компоновки слегка различаются в разных дистрибутивах, поэтому, несмотря на их установку, вам, возможно, придется поэкспериментировать немного, чтобы выяснить, как компилировать исходные файлы с использованием ndbm. Наиболее частый вариант — база данных gdbm установлена и поддерживает по умолчанию режим совместимости с версией ndbm. Дистрибутивы, например Red Hat, как правило, делают это. В этом случае вам нужно выполнить следующие шаги:

1. Включите в ваш файл на языке С файл ndbm.h.

2. Включите каталог заголовочного файла /usr/include/gdbm с помощью опции -I/usr/include/gdbm.

3. Скомпонуйте программу с библиотекой gdbm, используя опцию -lgdbm.

Если программа не работает, обычная альтернатива, принятая в новейших версиях дистрибутивов Ubuntu и SUSE, — устанавливается база данных gdbm, но при необходимости явно запрашивается совместимость с базой данных ndbm, и вы должны компоновать программу сначала с библиотекой совместимости, а затем с основной библиотекой. В этом случае надо выполнить следующие шаги:

1. Вместо файла ndbm.h включите в ваш файл на С файл gdbm-ndbrh.h.

2. Включите каталог заголовочного файла /usr/include/gdbm с помощью опции -I/usr/include/gdbm.

3. Скомпонуйте программу с дополнительной библиотекой совместимости gdbm, используя опцию -lgdbm_compat -lgdbm.

Загружаемый Makefile и С-файлы dbm установлены с первым вариантом, принятым по умолчанию, но содержат комментарии о том, как их отредактировать, чтобы можно было легко выбрать второй вариант. В оставшейся части главы мы полагаем, что в вашей системе совместимость с ndbm — характеристика, принятая по умолчанию.

Подпрограммы dbm

Как и библиотека curses, обсуждавшаяся нами в главе 6, средство dbm состоит из заголовочного файла и библиотеки, которая должна компоноваться с программой во время компиляции последней. Библиотека называется просто dbm, но поскольку мы обычно применяем в системе Linux реализацию GNU, необходимо компоновать с этой реализацией, используя в строке компиляции опцию -lgdbm. Заголовочный файл — ndbm.h.

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

Основной элемент базы данных dbm — блок данных, предназначенных для хранения, связанный с блоком данных, действующих как ключ для извлечения данных. У всех баз данных dbm должны быть уникальные ключи для каждого хранящегося блока данных. Значение ключа используется как индекс хранящихся данных. Нет ограничений на ключи или данные и не определено никаких ошибок при использовании данных или ключей слишком большого размера. Стандарт допускает реализацию, ограничивающую размер ключа/данных величиной 1023 байта, но, как правило, ограничений не существует, поскольку реализации оказались более гибкими, чем требования, предъявляемые к ним.

Для манипулирования этими блоками как данными в заголовочном файле ndbm.h определен новый тип данных, названный datum. Конкретное содержимое этого типа зависит от реализации, но он должен, как минимум, включать следующие элементы:

void *dptr;

size_t dsize

Здесь datum — тип, который будет определяться оператором typedef. В файле ndbm.h также дано определение dbm, представляющее собой структуру, применяемую для доступа к базе данных, и во многом похожее на определение FILE, используемое для доступа к файлам. Внутреннее содержимое dbm typedef зависит от реализации и никогда не должно использоваться.

Для ссылки на блок данных при использовании библиотеки dbm вы должны объявить datum, задать указатель dptr для указания на начало данных, а также задать параметр dsize, содержащий размер данных. На хранящиеся данные и индекс, применяемый для доступа к ним, всегда нужно ссылаться с помощью типа datum.

О типе DBM лучше всего думать как об аналоге типа FILE. Когда вы открываете базу данных dbm, обычно создаются два физических файла: один с расширением pag, а другой с расширением dir. Возвращается один указатель dbm, который применяется для обращения к обоим файлам как к паре. Файлы никогда не следует непосредственно читать и в них не нужно писать; они предназначены для доступа через стандартные операции dbm.

Примечание

В некоторых реализациях эти два файла объединены, и создается один новый файл.

Если вы знакомы с базами данных SQL, то заметите, что в случае базы данных dbm не существует структур таблиц или столбцов. Эти структуры не нужны, т.к. dbm не задает фиксированного размера элементов сохраняемых данных и не требует описания внутренней структуры для них. Библиотека dbm работает с блоками неструктурированных двоичных данных.

Функции доступа dbm

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

#include <ndbm.h>

DBM *dbm_open(const char* filename, int file_open_flags,

 mode_t file_mode);

int dbm_store(DBM *database_descriptor, datum key, datum content,

 int store_mode);

datum dbm_fetch(DBM* database descriptor, datum key);

void dbm_close(DBM *database descriptor);

dbm_open

Эта функция применяется для открытия имеющихся баз данных и для создания новых баз данных. Аргумент filename — имя файла базы данных без расширения dir или pag.

Остальные параметры такие же, как второй и третий параметры функции open, с которой вы встречались в главе 3. Вы можете использовать те же директивы #define. Второй аргумент управляет возможностью чтения базы данных, записью в нее или обеими операциями. Если создается новая база данных, флаги должны быть двоичными O_READ с O_CREAT, чтобы разрешить создание файлов. Третий аргумент задает начальные права доступа к файлам, которые будут созданы.

Функция dbm_open возвращает указатель на тип DBM. Он применяется во всех последующих обращениях к базе данных. В случае аварийного завершения возвращается (DBM*)0.

dbm_store

Эту функцию применяют для ввода данных в базу данных. Как упоминалось ранее, все данные должны сохраняться с уникальным индексом. Для определения данных, которые вы хотите сохранить, и индекса, используемого для ссылки на них, следует задать два типа datum: один для ссылки на индекс, а другой — на реальные данные. Последний параметр store_mode управляет действиями, совершаемыми при попытке сохранить какие-либо данные с применением ключа, который уже существует. Если установлено значение параметра dbm_insert, сохранение завершается аварийно и функция dbm_store возвращает 1. Если установлено значение параметра dbm_replace, новые данные заменяют существующие и dbm_store возвращает 0. При возникновении других ошибок функция dbm_store возвращает отрицательные числа.

dbm_fetch

Подпрограмма dbm_fetch применяется для извлечения данных из базы данных. Она принимает в качестве параметра указатель dbm, возвращенный предшествующим вызовом функции dbm_open и тип datum, который должен быть задан как указатель на ключ. Тип datum возвращается, если данные, относящиеся к используемому ключу, найдены в базе данных, возвращаемая структура datum будет иметь значения dptr и dsize, ссылающиеся на возвращенные данные. Если ключ не найден, dptr будет равен null.

Примечание

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

dbm_close

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

А теперь выполните упражнение 7.12.

Упражнение 7.12. Простая база данных dbm

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

1. Первыми представлены файлы #include, директивы #define, функция main и объявление структуры test_data:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <ndbm.h>

/* В некоторых системах вам нужно заменить вышестоящую строку строкой #include <gdbm-ndbm.h>*/

#include <string.h>

#define TEST_DB_FILE "/tmp/dbm1_test"

#define ITEMS_USED 3

struct test_data {

 char misc_chars[15];

 int any_integer;

 char more_chars[21];

};

int main() {

2. В функции main задайте элементы структур items_to_store и items_received, строку key и типы datum:

 struct test_data items_to_store[ITEMS_USED];

 struct test_data item_retrieved;

 char key_to_use[20];

 int i, result;

 datum key_datum;

 datum data_datum;

 DBM *dbm_ptr;

3. Объявив указатель на структуру типа DBM, откройте вашу тестовую базу данных для чтения и записи, создав ее при необходимости:

 dbm_ptr = dbm_open(TEST_DB_FILE, O_RDWR | O_CREAT, 0666);

 if (!dbm_ptr) {

  fprintf (stderr, "Failed to open database\n");

  exit(EXIT_FAILURE);

 }

4. Теперь добавьте данные в структуру items_to_store:

 memset(items_to_store, '\0', sizeof(items_to_store));

 strcpy(items_to_store[0].misc_chars, "First! ");

 items_to_store[0].any_integer = 47;

 strcpy(items_to_store[0].more_chars, "foo");

 strcpy(items_to_store[1].misc_chars, "bar");

 items_to_store[1].any_integer = 13;

 strcpy(items_to_store[1].more_chars, "unlucky? ");

 strcpy(items_to_store[2].misc_chars, "Third");

 items_to_store[2].any_integer = 3;

 strcpy(items_to_store[2].more_chars, "baz");

5. Для каждого элемента необходимо сформировать ключ для будущих ссылок в виде первой буквы каждой строки и целого числа. Этот ключ затем будет обозначен key_datum, когда data_datum сошлется на элемент items_to_store. Далее вы сохраняете данные в базе данных:

 for (i = 0; i < ITEMS_USED; i++) {

  sprintf(key_to_use, "%c%c%d",

   items_to_store[i].misc_chars[0], items_to_store[i].more_chars[0], items_to_store[i].any_integer);

  key_datum.dptr = (void*)key_to_use;

  key_datum.dsize = strlen(key to_use);

  data_datum.dptr = (void*)&items_to_store[i];

  data_datum.dsize = sizeof(struct.test_data);

  result = dbm_store(dbm_ptr, key_datum, data_datum, DBM_REPLACE);

  if (result != 0) {

   fprintf(stderr, "dbm_store failed on key %s\n", key_to_use);

   exit(2);

  }

 }

6. Далее посмотрите, сможете ли вы извлечь эти новые данные, и в заключение следует закрыть базу данных:

 sprintf(key_to_use, "bu%d", 13);

 key_datum.dptr = key_to_use;

 key_datum.dsize = strlen(key_to_use);

 data_datum = dbm_fetch(dbm_ptr, key_datum);

 if (data_datum.dptr) {

  printf("Data retrieved\n");

  memcpy(&item_retrieved, data_datum.dptr, data_datum.dsize);

  printf("Retrieved item — %s %d %s\n", item_retrieved.misc_chars,

   item_retrieved.any_integer, item_retrieved.more_chars);

 } else {

  printf("No data found for key %s\n", key_to_use);

 }

 dbm_close(dbm_ptr);

 exit(EXIT_SUCCESS);

}

Когда вы откомпилируете и выполните программу, вывод будет следующим:

$ gcc -о dbm1 -I/usr/include/gdtm dbm1.с -lgdbm

$ ./dbm1

Data retrieved

Retrieved item — bar 13 unlucky?

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

$ gcc -о dbm1 -I/usr/include/gdbm dbm1.с -lgdbm_compat -lgdbm

Как это работает

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

Далее вы задаете две структуры типа datum, одну для ключа и другую для сохраняемых данных. Сохранив три элемента в базе данных, вы конструируете новый ключ и настраиваете структуру типа datum так, чтобы она указывала на него. Затем вы применяете данный ключ для извлечения данных из базы данных. Убедитесь в успехе, проверив, что dptr в возвращенном datum не равен null. Получив подтверждение, вы можете копировать извлеченные данные (которые могут храниться внутри библиотеки dbm) в вашу собственную структуру, применяя возвращенный размер dbm_fetch (если этого не сделать, при наличии данных переменного размера вы можете скопировать несуществующие данные). В заключение извлеченные данные выводятся на экран, чтобы продемонстрировать корректность их извлечения.

Дополнительные функции dbm

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

int dbm_delete(DBM *database_descriptor, datum key);

int dbm_error(DBM *database_descriptor);

int dbm_clearerr(DBM *database_dascriptor);

datum dbm_firstkey(DBM *database_descriptor);

datum dbm_nextkey(DBM *database_descriptor);

dbm_delete

Функция dbm_delete применяется для удаления элементов из базы данных. Она принимает ключ типа datum точно так же, как функция dbm_fetch, но вместо извлечения данных она удаляет их. В случае успешного завершения функция возвращает 0.

dbm_error

Функция dbm_error просто проверяет базу данных на наличие ошибок, возвращая 0 при их отсутствии.

dbm_clearerr

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

dbm_firstkey и dbm_nextkey

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

DBM *db_ptr;

datum key;

for (key = dbm_firstkey(db_ptr); key.dptr; key = dbm_nextkey(db_ptr));

Выполните упражнение 7.13.

Упражнение 7.13. Извлечение и удаление

В этом примере вы улучшите файл dbm1.с с помощью описанных новых функций и создадите новый файл dbm2.c.

1. Сделайте копию dbm1.с и откройте его для редактирования. Отредактируйте строку #define TEST_DB_FILE.

#unclude <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <ndbm.h>

#include <string.h>

#define TEST_DB_FILE "/tmp/dbm2_test"

#define ITEMS_USED 3

2. Теперь вам нужно внести изменения только в секцию извлечения:

 /* теперь попытайтесь удалить некоторые данные */

 sprintf(key_to_use, "bu%d", 13);

 key_datum.dptr = key_to_use;

 key_datum.dsize = strlen(key_to_use);

 if (dbm_delete(dbm_ptr, key_datum) == 0) {

  printf("Data with key %s deleted\n", key_to_use);

 } else {

  printf("Nothing deleted for key %s\n", key_to_use);

 }

 for (key_datum = dbm_firstkey(dbm_ptr);

  key_datum.dptr;

  key_datum = dbm_nextkey(dbm_ptr)) {

  data_datum = dbm_fetch(dbm_ptr, key_datum);

  if (data_datum.dptr) {

   printf("Data retrieved\n");

   memcpy(&item_retrieved, data_datum.dptr, data_datum.dsize);

   printf("Retrieved item - %s %d %s\n",

    item_retrieved.misc_chars, item_retrieved.any_integer,

    item_retrieved.more_chars);

  } else {

   printf("No data found for key %s\n", key_to_use);

  }

 }

}

Далее приведен вывод:

$ ./dbm2

Data with key bu13 deleted

Data retrieved

Retrieved item — Third 3 baz

Data retrieved

Retrieved item - First! 47 foo

Как это работает

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

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

Приложение для работы с коллекцией компакт-дисков

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

Обновление проектного решения

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

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

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

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

С базой данных dbm вы не сможете применять язык SQL, но опишите новую базу данных с помощью более формальных терминов, используя терминологию языка SQL. Не волнуйтесь, если вы не знакомы с этим языком, мы поясним все определения, а в главе 8 вы узнаете о нем больше. В программном коде таблица может быть описана следующим образом:

CREATE TABLE cdc_entry (

 catalog CHAR(30) PRIMARY KEY REFERENCES cdt_entry(catalog),

 title CHAR(70),

 type CHAR(30),

 artist CHAR(70)

);

CREATE TABLE cdt_entry (

 catalog CHAR(30) REFERENCES cdc_entry(catalog),

 track_no INTEGER,

 track_txt CHAR(70),

 PRIMARY KEY(catalog, track_no)

);

Это очень краткое описание сообщает имена и размеры полей. В таблице cdc_entry у каждого элемента есть уникальный столбец каталога catalog. В таблице cdt_entry номер дорожки не может быть нулевым и комбинация столбцов catalog и track_no уникальна. Вы увидите их определение в виде структур typedef struct в следующем разделе программного кода.

Приложение управления базой данных компакт-дисков, использующее dbm

Сейчас заново вы создадите приложение, использующее базу данных dbm для хранения нужной вам информации, в виде файлов cd_data.h, app_ui.c и cd_access.c (упражнения 7.14–7.16).

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

Примечание

В последующих главах вы не раз встретитесь с заголовочным файлом базы данных cd_data.h и функциями из файла cd_access.c. Помните о том, что некоторые дистрибутивы Linux требуют немного отличающихся формирующих опций, например, применения в вашем файле на языке С заголовочного файла gdbm-ndbm.h вместо файла ndbm.h и опций -lgdbm_compat -lgdbm вместо просто опции -lgdbm. Если в вашем дистрибутиве Linux дело обстоит именно так, вы должны внести соответствующие изменения в файлы access.с и Makefile.

Упражнение 7.14. Файл cd_data.h

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

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

/* Таблица catalog */

#define CAT_CAT_LEN 30

#define CAT_TITLE_LEN 70

#define CAT_TYPE_LEN 30

#define CAT_ARTIST_LEN 70

typedef struct {

 char catalog[CAT_CAT_LEN + 1];

 char title[CAT_TITLE_LEN + 1];

 char type [CAT_TYPE_LEN + 1];

 char artist[CAT_ARTIST_LEN + 1];

} cdc_entry;

/* Таблица дорожек, по одному элементу на дорожку */

#define TRACK_CAT_LEN CAT_CAT_LEN

#define TRACK_TTEXT_LEN 70

typedef struct {

 char catalog[TRACK_CAT_LEN + 1];

 int track_no;

 char track_txt[TRACK_TTEXT_LEN + 1];

} cdt_entry;

2. Теперь, имея структуры данных, можно определить нужные вам подпрограммы доступа. Функции с префиксом cdc_ в имени предназначены для элементов каталога, с префиксом cdt_ — для элементов-дорожек.

Примечание

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

/* Функции инициализации и завершения */

int database_initialize(const int new_database);

void database_close(void);

/* Две функции для простого извлечения данных */

cdc_entry get_cdc_entry(const char *cd_catalog_ptr);

cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no);

/* Две функции для добавления данных */

int add_cdc_entry(const cdc_entry entry_to_add);

int add_cdt_entry(const cdt_entry entry_to_add);

/* Две функции для удаления данных */

int del_cdc_entry(const char *cd_catalog_ptr);

int del_cdt_entry(const char *cd_catalog_ptr, const int track_no);

/* Одна функция поиска */

cdc_entry search_cdc_entry(const char *cd_catalog_ptr,

 int *first_call_ptr);

Упражнение 7.15. Файл app_ui.c

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

1. Как обычно, начните с некоторых заголовочных файлов:

#define _XOPEN_SOURCE

#include <stdlib.h>

#include <unistd.h>

#include <stdio.h>

#include <string.h>

#include "cd_data.h"

#define TMP_STRING_LEN 125 /* это число должно быть больше

                              самой длинной строки в структуре базы данных */

2. Опишите пункты вашего меню с помощью typedef. Этот вариант лучше применения констант, заданных в директивах #define, т.к. позволяет компилятору проверить типы переменных, задающих пункт меню.

typedef enum {

 mo_invalid,

 mo_add_cat,

 mo_add_tracks,

 mo_del_cat,

 mo_find_cat,

 mo_list_cat_tracks,

 mo_del_tracks,

 mo_count_entries,

 mo_exit

} menu_options;

3. Теперь введите прототипы локальных функций. Помните о том, что прототипы функций, обеспечивающих реальный доступ к базе данных, включены в файл cd_data.h.

static int command_mode(int argc, char *argv[]);

static void announce(void);

static menu_options show_menu(const cdc_entry *current_cdc);

static int get_confirm(const char *question);

static int enter_new_cat_entry(cdc_entry *entry_to_update);

static void enter_new_track_entries(const cdc_entry* entry_to_add_to);

static void del_cat_entry(const cdc_entry *entry_to_delete);

static void del_track_entries(const cdc_entry *entry_to_delete);

static cdc_entry find_cat(void);

static void list_tracks(const cdc_entry *entry_to_use);

static void count_all_entries(void);

static void display_cdc(const cdc_entry *cdc_to_show);

static void display_cdt(const cdt_entry *cdt_to_show);

static void strip_return(char *string_to_strip);

4. И наконец, вы добрались до функции main. Она начинается с проверки того, что текущий элемент current_cdc_entry, который применяется для сохранения дорожки выбранного в данный момент элемента каталога компакт-дисков, инициализирован. Также проводится грамматический разбор командной строки, выдается оповещение о том, какая программа выполняется, и инициализируется база данных.

void main(int argc, char *argv[]) {

 menu_options current_option;

 cdc_entry current_cdc_entry;

 int command_result;

 memset(&current_cdc_entry, '\0', sizeof(current_cdc_entry));

if (argc >1) {

  command_result = command_mode(argc, argv);

  exit(command_result);

 }

 announce();

 if (!database_initialize(0)) {

  fprintf(stderr, "Sorry, unable to initialize database\n");

  fprintf(stderr, "To create a new database use %s -i\n", argv[0]);

  exit(EXIT_FAILURE);

 }

5. Теперь вы готовы обрабатывать ввод пользователя. Вы остаетесь в цикле, запрашивая пункт меню и обрабатывая его до тех пор, пока пользователь не выберет выход. Обратите вниманий на то, что вы передаете структуру current_cdc_entry в функцию show_menu, чтобы разрешить изменять варианты пунктов меню, когда выбран текущий элемент каталога:

 while (current_option != mo_exit) {

  current_option = show_menu(&current_cdc_entry);

  switch(current_option) {

  case mo_add_cat:

   if (enter_new_cat_entry(&current_cdc_entry)) {

    if (!add_cdc_entry(current_cdc_entry)) {

     fprintf(stderr, "Failed to add new entry\n");

     memset(&current_cdc_entry, '\0',

      sizeof(current_cdc_entry));

    }

   }

   break;

  case mo_add_tracks:

   enter_new_track_entries(&current_cdc_entry);

   break;

  case mo_del_cat:

   del_cat_entry(&current_cdc_entry);

   break;

  case mo_find_cat:

   current_cdc_entry = find_cat();

   break;

  case mo_list_cat_tracks:

   list_tracks(&current_cdc_entry);

   break;

  case mo_del_tracks:

   del_track_entries(&current_cdc_entry);

   break;

  case mo_count_entries:

   count_all_entries();

   break;

  case mo_exit:

   break;

  case mo_invalid:

   break;

  default:

   break;

  } /* switch */

 } /* while */

6. Когда цикл в функции main завершится, закройте базу данных и вернитесь в окружение. Функция announce выводит приглашающее предложение:

 database_close();

 exit(EXIT_SUCCESS);

} /* main */

static void announce(void) {

 printf("\n\nWelcome to the demonstration CD catalog database \

  program\n");

}

7. Здесь вы реализуете функцию show_menu. Эта функция проверяет, выбран ли текущий элемент каталога, используя первый символ имени в каталоге. Если элемент каталога выбран, становятся доступными дополнительные пункты меню:

static menu_options show_menu(const cdc_entry *cdc_selected) {

 char tmp_str[TMP_STRING_LEN + 1];

 menu_options option_chosen = mo_invalid;

 while (option_chosen == mo_invalid) {

  if (cdc_selected->catalog[0]) {

   printf("\n\nCurrent entry: ");

   printf("%s, %s, %a, %s\n",

    cdc_selected->catalog, cdc_selected->title,

    cdc_selected->type, cdc_selected->artist);

   printf("\n");

   printf("1 - add new CD\n");

   printf("2 — search for a CD\n");

   printf("3 — count the CDs and tracks in the database\n");

   printf("4 — re-enter tracks for current CD\n");

   printf("5 - delete this CD, and all its tracks\n");

   printf("6 - list tracks for this CD\n");

   printf("q — quit\n");

   printf("\nOption: ");

   fgets(tmp_str, TMP_STRING_LEN, stdin);

   switch(tmp_str[0]) {

   case '1':

    option_chosen = mo_add_cat;

    break;

   case '2':

    option_chosen = mo_find_cat;

    break;

   case '3':

    option_chosen = mo_count_entries;

    break;

   case '4':

    option_chosen = mo_add_tracks;

    break;

   case '5':

    option_chosen = mo_del_cat;

    break;

   case '6':

    option_chosen = mo_list_cat_tracks;

    break;

   case 'q':

    option_chosen = mo_exit;

    break;

   }

  } else {

   printf("\n\n");

   printf("1 - add new CD\n");

   printf("2 - search for a CD\n");

   printf("3 — count the CDs and tracks in the database\n");

   printf("q — quit\n");

   printf("\nOption: ");

   fgets(tmp_str, TMP_STRING_LEN, stdin);

   switch(tmp_str[0]) {

   case '1':

    option_chosen = mo_add_cat;

    break;

   case '2':

    option_chosen = mo_find_cat;

    break;

   case '3':

    option_chosen = mo_count_entries;

    break;

   case 'q':

    option_chosen = mo_exit;

    break;

   }

  }

 } /* while */

 return(option_chosen);

}

Примечание

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

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

static int get_confirm(const char *question) {

 char tmp_str[TMP_STRING_LEN + 1];

 printf("%s", question);

 fgets(tmp_str, TMP_STRING_LEN, stdin);

 if (tmp_str[0] == 'Y' || tmp_str[0] = 'y') {

  return(1);

 }

 return(0);

}

9. Функция enter_new_cat_entry позволяет вводить новый элемент каталога. Вам не нужно сохранять перевод строки, который возвращает функция fgets, поэтому отбросьте его:

static int enter_new_cat_entry(cdc_entry *entry_to_update) {

 cdc_entry new_entry;

 char tmp_str[TMP_STRING_LEN + 1];

 memset(&new_entry, '\0', sizeof(new_entry));

 printf("Enter catalog entry: ");

 (void)fgets(tmp_str, TMP_STRING_LEN, stdin);

 strip_return(tmp_str);

 strncpy(new_entry.catalog, tmp_str, CAT_CAT_LEN - 1);

 printf("Enter title: ");

 (void)fgets(tmp_str, TMP_STRING_LEN, stdin);

 strip_return(tmp_str);

 strncpy(new_entry.title, tmp_str, CAT_TITLE_LEN - 1);

 printf("Enter type: ");

 (void)fgets(tmp_str, TMP_STRING_LEN, stdin);

 strip_return(tmp_str);

 strncpy(new_entry.type, tmp_str, CAT_TYPE_LEN - 1);

 printf("Enter artist: ");

 (void)fgets(tmp_str, TMP_STRING_LEN, stdin);

 strip_return(tmp_str);

 strncpy(new_entry.artist, tmp_str, CAT_ARTIST_LEN - 1);

 printf("\nNew catalog entry entry is :-\n");

 display_cdc(&new_entry);

 if (get_confirm("Add this entry ? ")) {

  memcpy(entry_to_update, &new_entry, sizeof(new_entry));

  return(1);

 }

 return(0);

}

Примечание

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

10. Теперь вы переходите к функции enter_new_track_entries для ввода информации о дорожке. Эта функция немного сложнее функции ввода элемента каталога, поскольку вы разрешаете существующему элементу-дорожке оставаться неизменным:

static void enter_new_track_entries(const cdc_entry *entry_to_add_to) {

 cdt_entry new_track, existing_track;

 char tmp_str[TMP_STRING_LEN + 1];

 int track_no = 1;

 if (entry_to_add_to->catalog[0] == '\0') return;

 printf("\nUpdating tracks for %s\n", entry_to_add_to->catalog);

 printf("Press return to leave existing description unchanged, \n");

 printf(" a single d to delete this and remaining tracks, \n");

 printf(" or new track description\n");

 while(1) {

11. Сначала вы должны проверить, существует ли уже дорожка с текущим номером дорожки. В зависимости от результатов проверки меняется строка приглашения:

  memset(&new_track, '\0', sizeof(new_track));

  existing_track = get_cdt_entry(entry_to_add_to->catalog,

   track_no);

  if (existing_track.catalog[0]) {

   printf("\tTrack %d: %s\n", track_no,

    existing_track.track_txt);

   printf("\tNew text: ");

  } else {

   printf("\tTrack %d description: ", track_no);

  }

  fgets(tmp_str, TMP_STRING_LEN, stdin);

  strip_return(tmp_str);

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

  if (strlen(tmp_str) == 0) {

   if (existing_track.catalog[0] == '\0') {

    /* Нет в наличии элемента, поэтому вставка завершается */

    break;

   } else {

    /* Оставляем существующий элемент,

       переходам к следующей дорожке */

    track_no++;

    continue;

   }

  }

13. Если пользователь введет единичный символ d, это приведет к удалению текущей дорожки и дорожек с большими номерами. Функция del_cdt_entry вернет false, если не сможет найти дорожку, которую следует удалить:

  if ((strlen(tmp_str) == 1) && tmp_str[0] == 'd') { /* Удаляет эту и оставшиеся дорожки */

   while (del_cdt_entry(entry_to_add_to->catalog, track_no)) {

    track_no++;

   }

   break;

  }

14. В этом пункте приводится код для вставки новой дорожки или обновления существующей. Вы формируете элемент cdt_entry структуры new_track и затем вызываете функцию базы данных add_cdt_entry для того, чтобы включить его в базу данных:

  strncpy(new_track. track_txt, tmp_str, TRACK_TTEXT_LEN - 1);

  strcpy(new_track.catalog, entry_to_add_to->catalog);

  new_track.track_no = track_no;

  if (!add_cdt_entry(new_track)) {

   fprintf(stderr, "Failed to add new track\n");

   break;

  }

  track_no++;

 } /* while */

}

15. Функция del_cat_entry удаляет элемент каталога. Никогда не разрешайте хранить дорожки для несуществующего элемента каталога.

static void del_cat_entry(const cdc_entry *entry_to_delete) {

 int track_no = 1;

 int delete_ok;

 display_cdc(entry_to_delete);

 if (get_confirm("Delete this entry and all it's tracks? ")) {

  do {

   delete_ok = del_cdt_entry(entry_to_delete->catalog, track_no);

   track_no++;

  } while(delete_ok);

  if (!del_cdc_entry(entry_to_delete->catalog)) {

   fprintf(stderr, "Failed to delete entry\n");

  }

 }

}

16. Следующая функция — утилита для удаления всех дорожек элемента каталога:

static void del_track_entries(const cdc_entry *entry_to_delete) {

 int track_no = 1;

 int delete_ok;

 display_cdc(entry_to_delete);

 if (get_confirm("Delete tracks for this entry? ")) {

  do {

   delete_ok = del_cdt_entry(entry_to_delete->catalog, track_no);

   track_no++;

  } while(delete_ok);

 }

}

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

static cdc_entry find_cat(void) {

 cdc_entry item_found;

 char tmp_str[TMP_STRING_LEN + 1];

 int first_call = 1;

 int any_entry_found = 0;

 int string ok;

 int entry_selected = 0;

 do {

  string_ok = 1;

  printf("Enter string to search for in catalog entry: ");

  fgets(tmp_str, TMP_STRING_LEN, stdin);

  strip_return(tmp_str);

  if (strlen(tmp_str) > CAT_CAT_LEN) {

   fprintf(stderr, "Sorry, string too long, maximum %d \

    characters\n", CAT_CAT_LEN);

   string_ok = 0;

  }

 } while (!string_ok);

 while (!entry_selected) {

  item_found = search_cdc_entry(tmp_str, &firstcall);

  if (item_found.catalog[0] != '\0') {

   any_entry_found = 1;

   printf("\n");

   display_cdc(&item_found);

   if (get_confirm("This entry? ")) {

    entry_selected = 1;

   }

  } else {

   if (any_entry_found) printf("Sorry, no more matches found\n");

   else printf("Sorry, nothing found\n");

   break;

  }

 }

 return(item_found);

}

18. Функция list_tracks — утилита, которая выводит все дорожки для заданного элемента каталога:

static void list_tracks(const cdc_entry *entry_to_use) {

 int track_no = 1;

 cdt_entry entry_found;

 display_cdc(entry_to_use);

 printf("\nTracks\n");

 do {

  entry_found = get_cdt_entry(entry_to_use->catalog, track_no);

  if (entry_found.catalog[0]) {

   display_cdt(&entry_found);

   track_no++;

  }

 } while(entry_found.catalog[0]);

 (void)get_confirm("Press return");

} /* list_tracks */

19. Функция count_all_entries подсчитывает все дорожки:

static void count_all_entries(void) {

 int cd_entries_found = 0;

 int track_entries_found = 0;

 cdc_entry cdc_found;

 cdt_entry cdt_found;

 int track_no = 1;

 int first_time = 1;

 char *search_string = "";

 do {

  cdc_found = search_cdc_entry(search_string, &first_time);

  if (cdc_found.catalog[0]) {

   cd_entries_found++;

   track_no = 1;

   do {

    cdt_found = get_cdt_entry(cdc_found.catalog, track_no);

    if (cdt_found.catalog[0]) {

     track_entries_found++;

     track_no++;

    }

   } while (cdt_found.catalog[0]);

  }

 } while (cdc_found.catalog[0]);

 printf("Found %d CDs, with a total of %d tracks\n",

  cd_entries_found, track_entries_found);

 (void)get_confirm("Press return");

}

20. Теперь у вас есть утилита display_cdc для вывода элемента каталога:

static void display_cdc(const cdc_entry *cdc_to_show) {

 printf("Catalog: %s\n", cdc_to_show->catalog);

 printf("\ttitle: %s\n", cdc_to_show->title);

 printf("\ttype: %s\n", cdc_to_show->type);

 printf("\tartist: %s\n", cdc_to_show->artist);

}

и утилита display_cdt для отображения элемента-дорожки:

static void display_cdt(const cdt_entry *cdt_to_show) {

 printf("%d: %s\n", cdt_to_show->track_no,

  cdt_to_show->track_txt);

}

21. Служебная функция strip_return удаляет завершающий строку символ перевода строки. Помните о том, что Linux, как и UNIX, использует один символ перевода строки для обозначения конца строки.

static void strip_return(char *string_to_strip) {

 int len;

 len = strlen(string_to_strip);

 if (string_to_strip[len - 1] == '\n')

 string_to_strip[len - 1] = '\0';

}

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

static int command_mode(int argc, char *argv[]) {

 int c;

 int result = EXIT_SUCCESS;

 char *prog_name = argv[0];

 /* Эти внешние переменные используются функцией getopt */

 extern char *optarg;

 extern optind, opterr, optopt;

 while ((c = getopt(argc, argv, ":i")) != -1) {

  switch(c) {

  case 'i':

   if (!database_initialize(1)) {

    result = EXIT_FAILURE;

    fprintf(stderr, "Failed to initialize database\n");

   }

   break;

  case ':':

  case '?':

  default:

   fprintf(stderr, "Usage: %s [-i]\n", prog_name);

   result = EXIT_FAILURE;

   break;

  } /* switch */

 } /* while */

 return(result);

}

Упражнение 7.16. Файл cd_access.c

Теперь переходите к функциям доступа к базе данных dbm.

1. Как обычно, начните с нескольких файлов #include. Далее примените директивы #define для задания файлов, которые будут использоваться для хранения данных:

#define _XOPEN_SOURCE

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <string.h>

#include <ndbm.h>

/* В некоторых дистрибутивах файл в предыдущей строке может быть придется заменить на gdbm-ndbm.h */

#include "cd_data.h"

#define CDC_FILE_BASE "cdc_data"

#define CDT_FILE_BASE "cdt_data"

#define CDC_FILE_DIR "cdc_data.dir"

#define CDC_FILE_PAG "cdc_data.pag"

#define CDT_FILE_DIR "cdt_data.dir"

#define CDT_FILE_PAG "cdt_data.pag"

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

static DBM *cdc_dbm_ptr = NULL;

static DBM *cdt_dbm_ptr = NULL;

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

int database_initialize(const int new_database) {

 int open_mode = O_CREAT | O_RDWR;

 /* Если открыта какая-либо имеющаяся база данных, закрывает ее */

 if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);

 if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);

 if (new_database) {

  /* Удаляет старые файлы */

  (void)unlink(CDC_FILE_PAG);

  (void)unlink(CDC_FILE_DIR);

  (void)unlink(CDT_FILE_PAG);

  (void)unlink(CDT_FILE_DIR);

 }

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

 cdc_dbm_ptr = dbm_open(CDC_FILE_BASE, open_mode, 0644);

 cdt_dbm_ptr = dbm_open(CDT_FILE_BASE, open_mode, 0644);

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) {

  fprintf(stderr, "Unable to create database\n");

  cdc_dbm_ptr = cdt_dbm_ptr = NULL;

  return (0);

 }

 return (1);

}

4. Функция database_close просто закрывает базу данных, если она была открыта и устанавливает указатели базы данных в null, чтобы показать, что нет открытой базы данных:

void database_close(void) {

 if (cdc_dbm_ptr) dbm_close(cdc_dbm_ptr);

 if (cdt_dbm_ptr) dbm_close(cdt_dbm_ptr);

 cdc_dbm_ptr = cdt_dbm_ptr = NULL;

}

5. Далее у вас появляется функция, извлекающая единственный элемент каталога, когда передан указатель на строку текста из каталога. Если элемент не найден, у возвращенных данных пустое поле каталога:

cdc_entry get_cdc_entry(const char *cd_catalog_ptr) {

 cdc_entry entry_to_return;

 char entry_to_find[CAT_CAT_LEN + 1];

 datum local data datum;

 datum local_key_datum;

 memset(&entry_to_return, '\0', sizeof(entry_to_return));

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

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);

 if (!cd_catalog_ptr) return (entry_to_return);

 if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);

 memset(&entry_to_find, '\0', sizeof(entry_to_find));

 strcpy(entry_to_find, cd_catalog_ptr);

7. Задайте структуру datum, нужную функциям базы данных dbm, и используйте функцию dbm_fetch для извлечения данных. Если не извлечены никакие данные, вы возвращаете пустую структуру entry_to_return, которая была инициализирована ранее:

 local_key_datum.dptr = (void *) entry_to_find;

 local_key_datum.dsize = sizeof(entry_to_find);

 memset(&local_data_datum, '\0', sizeof(local_data_datum));

 local_data_datum = dbm_fetch(cdc_dbm_ptr, local_key_datum);

 if (local_data_datum.dptr) {

  memcpy(&entry_to_return, (char*)local_data_datum.dptr, local_data_datum.dsize);

 }

 return (entry_to_return);

} /* get_cdc_entry */

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

cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no) {

 cdt_entry entry_to_return;

 char entry_to_find[CAT_CAT_LEN + 10];

 datum local_data_datum;

 datum local_key_datum;

 memset(&entry_to_return, '\0', sizeof(entry_to_return));

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);

 if (!cd_catalog_ptr) return (entry_to_return);

 if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);

 /* Устанавливает ключ поиска, представляющий собой комбинацию

    элемента каталога и номера дорожки */

 memset(&entry_to_find, '\0', sizeof(entry_to_find));

 sprintf(entry_to_find, "%s %d", cd_catalog_ptr, track_no);

 local_key_datum.dptr = (void*)entry_to_find;

 local_key_datum.dsize = sizeof(entry_to_find);

 memset(&local_data_datum, '\0', sizeof(local_data_datum));

 local_data_datum = dbm_fetch(cdt_dbm_ptr, local_key_datum);

 if (local_data_datum.dptr) {

  memcpy(&entry_to_return, (char*)local_data_datum.dptr, local_data_datum.dsize);

 }

 return (entry_to_return);

}

9. Следующая функция add_cdc_entry добавляет новый элемент каталога:

int add_cdc_entry(const cdc_entry entry_to_add) {

 char key_to_add[CAT_CAT_LEN + 1];

 datum local_data_datum;

 datum local_key_datum;

 int result;

 /* Проверяет инициализацию базы данных и корректность параметров */

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);

 if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);

 /* Гарантирует включение в ключ поиска только корректной строки

    и значений null */

 memset(&key_to_add, '\0', sizeof(key_to_add));

 strcpy(key_to_add, entry_to_add.catalog);

 local_key_datum.dptr = (void*)key_to_add;

 local_key_datum.dsize = sizeof(key_to_add);

 local_data_datum.dptr = (void*)&entry_to_add;

 local_data_datum.dsize = sizeof(entry_to_add);

 result = dbm_store(cdc_dbm_ptr, local_key_datum, local_data_datum, DBM_REPLACE);

 /* dbm_store() применяет 0 для успешного завершения */

 if (result == 0) return (1);

 return (0);

}

10. Функция add_cdt_entry добавляет новый элемент-дорожку. Ключ доступа — это комбинация строки из каталога и номера дорожки:

int add_cdt_entry(const cdt_entry entry_to_add) {

 char key_to_add[CAT_CAT_LEN + 10];

 datum local_data_datum;

 datum local_key_datum;

 int result;

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);

 if (strlen(entry_to_add.catalog) >= CAT_CAT_LEN) return (0);

 memset(&key_to_add, '\0 ', sizeof(key_to_add));

 sprintf(key_to_add, "%s %d", entry_to_add.catalog, entry_to_add.track_no);

 local_key_datum.dptr = (void*)key_to_add;

 local_key_datum.dsize = sizeof(key_to_add);

 local_data_daturn.dptr = (void*)&entry_to_add;

 local_data_datum.dsize = sizeof(entry_to_add);

 result = dbm_store(cdt_dbm_ptr, local_key_datum, local_data_datum, DBM_REPLACE);

 /* dbm_store() применяет 0 в случае успешного завершения

    и отрицательные числа для обозначения ошибок */

 if (result == 0) return (1);

 return (0);

}

11. Если вы можете вставлять строки, было бы лучше, если вы могли бы и удалять их. Следующая функция удаляет элементы каталога;

int del_cdc_entry(const char *cd_catalog_ptr) {

 char key_to_del[CAT_CAT_LEN +1];

 datum local_key_datum;

 int result;

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);

 if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);

 memset(&key_to_del, '\0', sizeof(key_to_del));

 strcpy(key_to_del, cd_catalog_ptr);

 local_key_datum.dptr = (void*)key_to_del;

 local_key_datum.dsize = sizeof(key_to_del);

 result = dbm_delete(cdc_dbm_ptr, local_key_datum);

 /* dbm_delete() применяет 0 в случае успешного завершения */

 if (result == 0) return (1);

 return (0);

}

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

int del_cdt_entry(const char *cd_catalog_ptr, const int track_no) {

 char key_to_del[CAT_CAT_LEN + 10];

 datum local_key_datum;

 int result;

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (0);

 if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (0);

 memset(&key_to_del, '\0', sizeof(key_to_del));

 sprintf(key_to_del, "%s %d", cd_catalog_ptr, track_no);

 local_key_datum.dptr = (void*)key_to_del;

 local_key_datum.dsize = sizeof(key_to_del);

 result = dbm_delete(cdt_dbm_ptr, local_key_datum);

 /* dbm_delete() применяет 0 в случае успешного завершения */

 if (result == 0) return (1);

 return (0);

} 

13. И последнее, но не по значимости, у вас есть простая функция поиска. Она не очень замысловата, но, тем не менее, показывает, как просматривать элементы dbm, если ключи заранее неизвестны.

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

Когда вы хотите перезапустить поиск, возможно, указав другой элемент каталога, вы должны снова вызвать эту функцию с параметром *first_call_ptr, равным true, что приведет к выполнению нового поиска.

Между вызовами функция хранит некоторую внутреннюю информацию о состоянии. Это скрывает от клиента сложность продолжения поиска и защищает секреты реализации функции поиска.

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

cdc_entry search_cdc_entry(const char *cd_catalog_ptr,

 int *first_call_ptr) {

 static int local_first_call = 1;

 cdc_entry entry_to_return;

 datum local_data_datum;

 static datum local_key_datum; /* обратите внимание,

                                  должна быть static */

 memset(&entry_to_return, '\0', sizeof(entry_to_return));

14. Как всегда, начните с имеющих смысл проверок:

 if (!cdc_dbm_ptr || !cdt_dbm_ptr) return (entry_to_return);

 if (!cd_catalog_ptr || !first_call_ptr) return (entry_to_return);

 if (strlen(cd_catalog_ptr) >= CAT_CAT_LEN) return (entry_to_return);

 /* Защита от пропуска вызова с *first_call_ptr, равным true */

 if (local_first_call) {

  local_first_call = 0;

  *first_call_ptr = 1;

 }

15. Если эта функция была вызвана с параметром *first_call_ptr, равным true, вы должны продолжить (или заново начать) поиск от начала базы данных. Если *first_call_ptr не равен true, просто переходите к следующему ключу в базе данных:

 if (*first_call_ptr) {

  *first_call_ptr = 0;

  local_key_datum = dbm_firstkey(cdc_dbm_ptr);

 } else {

  local_key_datum = dbm_nextkey(cdc_dbm_ptr);

 }

 do {

  if (local_key_datum.dptr != NULL) {

   /* Элемент был найден */

   local_data_datum = dbm_fetch(cdc_dhm_ptr, local_key_datum);

   if (local_data_datum.dptr) {

    memcpy(&entry_to_return, (char*)local_data_datum.dptr, local_data_datum, dsize);

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

    /* Проверяет, входит ли строка в текущий элемент */

    if (!strstr(entry_to_return.catalog, cd_catalog_ptr)) {

     memset(&entry_to_return, '\0', sizeof(entry_to_return));

     local_key_datum = dbm_nextkey(cdc_dbm_ptr);

    }

   }

  }

 } while (local_key_datum.dptr && local_data_datum.dptr &&

    (entry_to_return.catalog[0] == '\0'));

 return (entry_to_return);

} /* search_cdc_entry */

Теперь вы готовы собрать все вместе с помощью следующего make-файла или файла сборки. Не слишком углубляйтесь в него сейчас, поскольку мы обсудим его работу в следующей главе. В данный момент просто наберите его и сохраните как Makefile.

all: application

INCLUDE=/usr/include/gdbm LIBS=gdbm

# В некоторых дистрибутивах вам, возможно, придется изменить предыдущую

# строку, чтобы включить библиотеку совместимости, как показано далее

# LIBS= -lgdbm_compat -lgdbm

CFIAGS=

app_ui.о: app_ui.с cd_data.h

 gcc $(CFLAGS) -c app_ui.c

access.о: access.с cd_data.h

 gcc $(CFLAGS) -I$(INCLUDE) -c access.с

application: app_ui.o access.о

 gcc $(CFLAGS) -o application app_ui.o access.о -l$(LIBS)

clean:

 rm -f application *.o

nodbmfiles:

 rm -f *.dir *.pag

Для компиляции вашего нового приложения управления коллекцией компакт- дисков наберите следующую команду в командной строке:

$ make

Если все пройдет нормально, выполняемый файл application будет откомпилирован и помещен в текущий каталог.

Резюме

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

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

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

Глава 8

MySQL

Теперь, когда вы изучили основы управления данными с помощью обычных файлов и затем посредством простой, но очень быстрой базы данных dbm, можно перейти к полнофункциональному средству работы с данными: СУРБД или системе управления реляционной базы данных (Relational Database Management System, RDBMS).

Два самых известных приложения СУРБД с открытым исходным кодом — это PostgreSQL и MySQL, хотя существует и множество других. Есть также много коммерческих СУРБД, таких как Oracle, Sybase и DB2, все они многофункциональны и могут действовать на различных платформах. Работающая только под управлением ОС Windows система Microsoft SQL Server — еще одно популярное средство на коммерческом рынке СУБД. У всех этих программных продуктов есть свои достоинства, но с учетом занимаемого пространства и принадлежности к программному обеспечению с открытым кодом авторы книги сосредоточились исключительно на СУРБД MySQL.

Появление MySQL восходит к 1984 г., а коммерческий вариант был разработан и поддерживается под покровительством компании MySQL АВ в течение последних нескольких лет. Поскольку СУРБД MySQL — это программное обеспечение с открытым исходным кодом, условия его использования часто смешивают с аналогичными условиями в других проектах с открытым программным кодом. Несмотря на то, что в большинстве случаев MySQL может применяться в соответствии с Общедоступной лицензией проекта GNU (GPL), есть обстоятельства, требующие покупки коммерческой лицензии для использования этого продукта. Следует внимательно проверить лицензионные требования на Web-сайте MySQL (www.mysql.com) и определить, какая редакция MySQL соответствует вашим потребностям.

Если вам нужна база данных с открытым программным кодом, а условия применения MySQL в соответствии с требованиями лицензии GPL для вас не приемлемы, и вы не хотите покупать коммерческую лицензию, то можете рассмотреть как альтернативу применение мощной СУРБД PostgreSQL (во время написания книги лицензионные условия использования PostgreSQL были менее строгими). Подробности можно найти на Web-сайте www.postgresql.org.

Примечание

Более подробную информацию о PostgreSQL вы можете найти в нашей книге: Neil Matthew. Beginning Databases with PostgreSQL: From Novice to Professional. Second Edition. — Apress, 2005. (Мэттью H. Базы данных на примере PostgreSQL: от новичка до профессионала. Второе издание).

В этой главе обсуждаются следующие темы:

□ установка MySQL;

□ команды администрирования, необходимые для работы с MySQL;

□ основные средства и функции MySQL;

□ API для взаимодействия ваших программ на языке С с базами данных MySQL;

□ создание реляционной базы данных, которую вы сможете применять в вашем приложении на языке С для управления коллекцией CD-дисков.

Установка

Какой бы вариант системы Linux вы не предпочли, вероятно, для него существует доступная версия MySQL, заранее откомпилированная и готовая к установке. Например, для Red Hat, SUSE и Ubuntu есть заранее откомпилированные пакеты, включенные в современные дистрибутивы этих ОС. Мы рекомендуем вам, как правило, применять заранее откомпилированные версии, поскольку они предоставляют самый легкий способ быстрых установки и запуска MySQL. Если в вашем дистрибутиве нет пакета MySQL или вы хотите получить самый свежий выпуск программного обеспечения, двоичные и исходные пакеты можно загрузить с Web-сайта MySQL.

В этой главе мы описываем установку только заранее откомпилированных версий MySQL.

Пакеты MySQL

Если по какой-то причине вам вместо стандартной версии нужно загрузить MySQL из Интернета, для подготовки и выполнения примеров из этой книги следует применять сборку Standard общедоступной версии (community edition). Вы увидите, что в нее включены пакеты Мах и Debug. Пакет Max содержит дополнительные средства, такие как поддержка необычных типов файлов для хранения и развитых средств, например кластеризации. Пакеты Debug откомпилированы с дополнительным кодом отладки и отладочной информацией; к счастью, вам не понадобится отладка на столь низком уровне.

Примечание

Не используйте версии Debug при эксплуатации; производительность снижается из-за дополнительной поддержки отладочных средств.

Для разработки приложений на базе MySQL вам придется установить не только сервер, но и библиотеки разработки. Как правило, в вашем диспетчере пакетов (package manager) есть вариант MySQL, нужно только убедиться в том, что установлены и библиотеки средств разработки. На рис. 8.1 показан диспетчер пакетов, готовый установить MySQL с дополнительным пакетом средств разработки, выделенным и готовым к установке.

Рис. 8.1 

В других дистрибутивах организация пакетов немного иная. Например, на рис. 8.2 показан объединенный диспетчер пакетов дистрибутива Ubuntu, готовый к установке MySQL.

Рис. 8.2

Установка MySQL также создает пользователя "mysql", имя которого по умолчанию применяется как имя процесса-демона сервера MySQL.

После установки пакетов необходимо проверить, запущена ли автоматически СУРБД MySQL. Во время написания книги некоторые дистрибутивы, например, Ubuntu делали это, в то время как другие, такие как Fedora, нет. К счастью, очень легко проверить, работает ли сервер MySQL:

$ ps -el | grep mysqld

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

Если нужно запустить (или перезапустить либо остановить) сервер MySQL, можно использовать панель управления сервисами GUI (GUI services control panel). Панель настройки сервисов (Service Configuration pane) дистрибутива Fedora показана на рис. 8.3.

Рис. 8.3

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

Настройка после установки

Предположим, что все идет как надо, СУБД MySQL установлена и стартовала с общим стандартным набором параметров. Это предположение можно проверить:

$ mysql -u root mysql

Если вы получите сообщение "Welcome to the MySQL monitor" ("Добро пожаловать в монитор MySQL") и затем приглашение mysql>, значит, сервер выполняется. Конечно, любой пользователь вмиг может подключиться к серверу и получить права администратора, но мы рассмотрим это лишь вкратце. Попробуйте ввести \s для получения некоторой дополнительной информации о вашем сервере. Когда насмотритесь, введите quit или \q для выхода из монитора.

Дополнительную информацию можно получить с помощью команды mysql -?, которая выводит еще больше подробностей, касающихся сервера. В выводе есть одна деталь, которую следует проверить. После списка аргументов обычно выводится строка Default options are read from the following files in the given order: (Текущие параметры считаны из следующих файлов в заданном порядке:). Она указывает, где найти файл конфигурации, который следует использовать для настройки вашего сервера MySQL. Стандартный файл конфигурации -— /etc/my.cnf, хотя в некоторых дистрибутивах, например Ubuntu, применяется файл /etc/mysql/my.cnf.

Состояние работающего сервера можно также проверить с помощью команды mysqladmin:

$ mysqladmin -u root version

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

Еще один полезный аспект применения команды mysqladmin — проверка конфигурационных параметров запущенного сервера с помощью опции variables:

$ mysqladmin variables

Эта команда выводит длинный список значений переменных. Пара особенно полезных — переменная datadir, сообщающая о том, где MySQL хранит данные, и переменная have_innodb, обычно равная YES и указывающая на то, что поддерживается универсальный механизм хранения данных (storage engine) InnoDB. MySQL поддерживает ряд механизмов хранения, представляющих собой низкоуровневую реализацию обработчиков для хранения данных. Наиболее популярные (и самые полезные) — InnoDB и MyISAM, но есть и другие, например механизм хранения в оперативной памяти (memory engine), совсем не использующий долговременную память, или CSV-механизм, применяющий файлы с переменными, разделенными запятыми. У разных механизмов хранения различные функции производительности. В настоящее время мы рекомендуем InnoDB как механизм хранения для баз данных общего назначения, представляющий собой компромиссное решение с точки зрения производительности и поддержки заданных связей между различными элементами данных. Если поддержка InnoDB не включена, проверьте файл конфигурации /etc/my.cnf, превратите в комментарий строку skip-innodb, поместив в начало строки знак номера или решетки (#), и воспользуйтесь редактором сервисов для перезапуска MySQL. Если это не поможет, возможно, у вас версия MySQL, откомпилированная без поддержки InnoDB, Поищите на Web-сайте MySQL версию с поддержкой InnoDB, если вам это важно. В примерах данной главы без ущерба можно применить и альтернативный механизм хранения MyISAM, во многих дистрибутивах используемый по умолчанию.

После того как вы убедитесь в том, что в двоичный файл сервера включена поддержка InnoDB, для того чтобы сделать его выбираемым по умолчанию механизмом хранения данных, вы должны задать его таковым в файле /etc/my.cnf, иначе по умолчанию будет применяться механизм хранения MyISAM. Редактирование очень простое: в раздел mysqld вставьте строку default-storage-engine=INNODB. К примеру, начало файла могло бы выглядеть следующим образом:

[mysqld]

default-storage-engine=INNODB

datadir=/var/lib/mysql

...

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

В процессе эксплуатации вам, как правило, придется также изменять установленное по умолчанию место хранения данных, задаваемое переменной datadir. Делается это тоже с помощью редактирования раздела mysql конфигурационного файла /etc/my.cnf. Например, если вы применяете механизм хранения InnoDB для размещения файлов данных в каталоге /vol02, а файлов регистрации — в каталоге /vol03 плюс задаете начальный размер файла данных 10 Мбайт с возможностью увеличения, можно использовать следующие конфигурационные строки:

innodb_data_home_dir = /vol02/mysql/data

innodb_data_file_path = ibdata1:10M:autoextend

innodb_log_group_home_dir = /vol03/mysql/logs

Более подробную информацию и другие конфигурационные параметры можно найти в интерактивных руководствах на Web-сайте www.mysql.com.

Примечание

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

Помните об имеющейся бреши в системе безопасности, упоминавшейся несколько разделов назад и позволяющей любому подключиться без пароля как пользователь root? Сейчас самое время усовершенствовать защиту. Не дайте сбить себя с толку имени пользователя root, применяемому во время установки MySQL. Между пользователем root СУРБД MySQL и пользователем root операционной системы нет никакой связи; MySQL просто регистрирует пользователя с именем "root" как администратора, что делает и ОС Linux. Пользователи базы данных MySQL и идентификаторы пользователей ОС Linux никак не связаны; у MySQL есть собственная встроенная система управления пользователями и правами доступа. По умолчанию пользователь с учетной записью в. вашей установленной системе Linux может зарегистрироваться на вашем сервере MySQL как администратор этой СУРБД. После того как вы ограничите права пользователя root СУРБД MySQL, например, разрешив только локальному пользователю регистрироваться с именем root и установив пароль для такого доступа, вы можете добавить только тех пользователей и только те права доступа, которые абсолютно необходимы для функционирования вашего приложения.

Установить пароль можно любым возможным способом, но самый простой с помощью команды:

$ mysqladmin -u root password newpassword

Она задает начальный пароль newpassword.

Этот метод порождает проблему, заключающуюся в том, что понятный текстовый пароль остается в протоколе или хронологии (history) вашей командной оболочки и может просматриваться кем угодно с помощью команды ps во время выполнения вашей команды или может быть извлечен из протокола команды. Лучше еще раз применить монитор MySQL на этот раз для отправки нескольких команд на языке SQL, которые изменят ваш пароль.

$ mysql -u root

Welcome to the MySQL monitor. Commands end with ; or \g.

Your MySQL connection id is 4

Type 'help;' or '\h' for help. Type ' \c' to clear the buffer.

mysql> SET password=PASSWORD('secretpassword');

Query OK, 0 rows affected (0.00 sec)

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

Примечание

Обратите внимание на то, что мы завершаем команды на языке SQL точкой с запятой (;). Строго говоря; она не является частью команды SQL, а применяется для того, чтобы сообщить программе-клиенту MySQL о том, что наша команда SQL готова к выполнению. Мы также пользуемся прописными буквами для ввода ключевых слов языка SQL, например, SET. Это не обязательно, потому что действительный синтаксис MySQL допускает ввод ключевых слов как прописными, так и строчными буквами, но мы применяем первый вариант как принятое соглашение в данной книге и в нашей повседневной работе, т.к. считаем, что это облегчает чтение команд SQL.

Теперь рассмотрим таблицу прав доступа, чтобы убедиться в том, что пароль установлен. Сначала с помощью команды use переключитесь на базу данных mysql и затем запросите внутренние таблицы:

mysql> use mysql

mysql> SELECT user, host, password FROM user;

+------+-----------+------------------+

| user | host      | password         |

+------+-----------+------------------+

| root | localhost | 2dxf8e9c23age6ed |

| root | fc7blp4e  |                  |

|      | localhost |                  |

|      | fc7blp4e  |                  |

+------+-----------+------------------+

4 rows in set (0.01 sec) mysql>

Отметьте, что вы создали пароль для пользователя root, только когда подключились с компьютера localhost. MySQL может хранить права доступа не только для пользователей, но и для классов соединений (connection classes), основанных на имени узла. Следующим шагом в защите вашей установки будет удаление ненужных пользователей, устанавливаемых MySQL по умолчанию. Приведенная далее команда удаляет из таблицы прав доступа всех пользователей с именами, отличающимися от root.

mysql> DELETE FROM user WHEREuser != 'root';

Query OK, 2 rows affected (0.01 sec)

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

mysql> DELETE FROM user WHEREhost != 'localhost';

Query OK, 1 row affected (0.01 sec)

И последнее, примените следующую команду для того, чтобы убедиться в отсутствии случайных регистраций:

mysql> SELECT user, host, password FROM user;

+------+-----------+------------------+

| user | host      | password         |

+------+-----------+------------------+

| root | localhost | 2dxf8e9c23age6ed |

+------+-----------+------------------+

1 row in set (0.00 sec) mysql> exit

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

Внимание! Момент истины: можете ли вы в дальнейшем регистрироваться с паролем, который установили? На сей раз вы задаете параметр -p, который сообщает MySQL о необходимости вывести подсказку для ввода пароля:

$ mysql -u root -p

Enter password:

Welcome to the MySQL monitor. Commands end with ; or \g.

Your MySQL connection id is 7

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql>

Теперь у вас есть работающая версия MySQL, заблокированная так, что только пользователь root с паролем, установленным вами, может подключиться к серверу базы данных и только с локальной машины. Подключиться к MySQL и ввести пароль вы можете из командной строки. Делается это с помощью параметра, --password, например, --password=secretpassword или -psecretpassword, но ясно, что это небезопасно, потому что пароль можно увидеть с помощью команды ps или просмотра хронологии команды. Однако ввод пароля в командной строке иногда просто необходим, например, если вы пишете сценарии, которым нужно подключаться к базе данных MySQL.

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

Как мы отмечали ранее, вы можете создать пользователей с различными правами подключения с разных машин; в примере пользователю root из соображений безопасности разрешено подключаться только с локальной машины. В данной главе создадим нового пользователя широкими правами доступа. Rick сможет подключаться тремя разными способами:

□ он сможет подключаться с локальной машины;

□ он сможет подключаться с любой машины, IP-адрес которой находится в диапазоне от 192.168.0.0 до 192.168.0.255;

□ он сможет подключаться с любой машины, входящей в домен wiley.com.

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

Создать пользователей и присвоить им полномочия можно с помощью команды grant:. Создадим пользователя с тремя только что перечисленными зонами, подключения. Ключевое слово IDENTIFIED BY — немного странная синтаксическая запись для задания начального пароля. Обратите внимание на способ применения кавычек; важно применять символы одинарных кавычек точно так, как показано, иначе вы не сможете создать пользователей, как намечали.

Подключитесь к MySQL как пользователь root и затем выполните следующие действия.

1. Создайте регистрацию входа с локальной машины (login) для пользователя rick.

mysql> GRANT ALL ON *.* TO rick@localhost IDENTIFIED BY 'secretpassword';

Query OK, 0 rows affected (0.03 sec)

2. Затем создайте регистрацию входа с любой машины из подсети класса С 192.168.0. Учтите, что следует использовать одинарные кавычки для защиты IP-диапазона и маску /255.255.255.0 для указания диапазона допустимых IP-адресов.

mysql> GRANT ALL ON *.* TO rick@'192.168.0.0/255.255.255.0' IDENTIFIED BY 'secretpassword';

Query OK, 0 rows affected (0.00 sec)

3. В заключение создайте такую регистрацию входа, чтобы пользователь rick мог зарегистрироваться с любой машины из домена wiley.com (и снова обратите внимание на одинарные кавычки).

mysql> GRANT ALL ON *.* ТО rick@'%.wiley.com' IDENTIFIED BY 'secretpassword';

Query OK, 0 rows affected. (0.00 sec)

4. Опять просмотрите таблицу пользователей, чтобы еще раз проверить все элементы!

mysql> SELECT user, host, password FROM mysql.user;

+------+---------------------------+------------------+

| user | host                      | password         |

+------+---------------------------+------------------+

| root | localhost                 | 2dxf8e8cl7ade6ed |

| rick | localhost                 | 3742g6348q8378d9 |

| rick | %.wiley.com               | 3742g6348q8378d9 |

| rick | 192.168.0.0/255.255.255.0 | 3742g6348q8378d9 |

+------+---------------------------+------------------+

4 rows in set (0.00 sec)

mysql>

Естественно, необходимо откорректировать предшествующие команды и пароли в соответствии с вашими локальными настройками. Вы должны были заметить команду GRANT ALL ON *.*, которая, как вы наверное догадались, предоставляет пользователю rick обширные права доступа. Это хорошо для опытного пользователя, но не годится для обычных пользователей. Мы более подробно обсудим команду grant в разд. "Создание пользователей и наделение их правами доступа" далее в этой главе, где среди прочего покажем, как создать пользователя с ограниченными правами доступа.

Теперь, когда вы установили и запустили СУРБД MySQL (если нет, см. следующий раздел), сделали установку более безопасной, и создали пользователя- неадминистратора, готового выполнять кое-какую работу, кратко обсудим поиск и устранение неисправностей после установки, а затем немного вернемся назад и дадим краткий обзор основ администрирования базы данных MySQL.

Устранение неисправностей после установки

Если при использовании mysql нет подключения к базе данных, проверьте с помощью системной команды ps, запущен ли серверный процесс. Если его нет в списке, попробуйте запустить mysql_safed -log. В этом случае в регистрационный каталог MySQL должен быть записан файл с дополнительной информацией. Можно конечно попытаться явно запустить процесс mysqld; используйте команду mysqld --verbose --help для получения полного списка опций командной строки.

Вполне возможно, что сервер функционирует, но просто отвергает ваше подключение. Если так, далее следует проверить наличие базы данных, особенно базы данных стандартных прав доступа MySQL (default permissions database). В дистрибутивах Red Hat она обычно по умолчанию располагается в /var/lib/mysqlis, другие дистрибутивы используют разные каталоги. Проверьте сценарий запуска MySQL (например, в файле /etc/init.d) и конфигурационный файл /etc/my.cnf. В противном случае запустите программу явно с помощью команды mysqld --verbose --help и найдите переменную datadir. После того как вы определили каталог базы данных, проверьте, содержит ли он хотя бы базу данных стандартных прав доступа (с именем mysql) и что именно ее, заданную в файле my.cnf, использует процесс-демон сервера.

Если вы все еще не подключились, воспользуйтесь редактором сервисов (service editor) для остановки сервера, убедитесь в том, что не выполняется ни один процесс mysqld, и затем перезапустите его снова и попробуйте подключиться. Если вы все- таки никуда не попали, можно полностью деинсталлировать MySQL и установить ее с нуля еще раз. Для выяснения некоторых известных только посвященным возможностей очень полезной может оказаться документация MySQL на Web-сайте (более свежая, чем интерактивное руководство на локальной машине, кроме того, в ней есть редактируемые пользователями подсказки, и предложения, и форум).

Администрирование MySQL

Группа программ-утилит, включенных в дистрибутив MySQL, облегчает процесс администрирования базы данных. Самая популярная из них — программа mysqladmin. В следующем разделе мы опишем эту и другие утилиты.

Команды

Все команды MySQL, за исключением mysqlshow, принимают как минимум три стандартных параметра, описанных в табл. 8.1.

Таблица 8.1

Опция команды Параметр Описание
-u username По умолчанию утилиты mysql будут пытаться использовать то же username MySQL, что и текущее имя пользователя Linux. Применяйте параметр -u для задания другого имени пользователя
[password] Если параметр задан, а пароль пропущен, он запрашивается. Если параметра -p нет в командной строке, команды MySQL полагают, что пароль не нужен
-h host Применяется для подключения к серверу на другой машине (для локальных серверов всегда можно опускать)

Примечание

И снова не советуем вам помещать пароль в командную строку, поскольку его можно увидеть с помощью команды ps.

myisamchk

Утилита myisamchk разработана для проверки и корректировки любых таблиц данных, применяющих стандартный табличный формат MYISAM, исходно поддерживаемый СУРБД MySQL. Обычно утилиту myisamchk следует запускать от имени пользователя mysql, созданного во время установки, из каталога, в котором размещаются таблицы. Для проверки базы данных выполните команду su mysql, измените название каталога в соответствии с именем базы данных и запустите утилиту myisamchk с одной или несколькими опциями, предложенными в табл. 8.2. Например,

myisamchk -e -r *.MYI

Самые популярные опции команды приведены в табл. 8.2.

Таблица 8.2

Опция команды Описание
Ищет ошибки в таблицах
-e Выполняет расширенную проверку
-r Исправляет найденные ошибки

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

mysql

Это основное и очень мощное средство командной строки СУРБД MySQL. С его помощью тем или иным способом можно выполнить любую административную или пользовательскую задачу. Запустить монитор mysql можно из командной строки; добавив заключительный дополнительный параметр, имя базы данных, вы сможете в мониторе обойтись без команды use <база_данных>. Далее приведен пример запуска монитора от имени пользователя rick, запроса пароля (обратите внимание на пробел после -p) и применения базы данных foo по умолчанию.

$ mysql -u rick -р foo

Для постраничного просмотра других опций командной строки монитора mysql примените команду mysql --help | less.

Если вы запускаете СУРБД MySQL без указания базы данных, для выбора одной из баз данных можно использовать опцию use <база_данных>, приведенную в списке команд в табл. 8.3.

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

$ mysql -u rick --password=secretpassword foo < sqlcommands.sql

После считывания и выполнения ваших команд mysql выведет на экран строку приглашения.

Во время подключения программы-клиента mysql к серверу в дополнение к стандартному набору команд SQL92 поддерживается ряд специфических команд, перечисленных в табл. 8.3.

Таблица 8.3

Команда Краткая форма Описание
help или ? \h или \? Отображает список команд
edit Редактирует команду. Применяемый редактор задается переменной окружения $EDITOR
exit или quit \q Завершает программу-клиент MySQL
go \g Выполняет команду
source <имя_файла> \. Выполняет команды SQL из заданного файла
status \s Отображает информацию о состоянии сервера
system <команда> \! Выполняет системную команду
tee <имя_файла> \T Добавляет в конец заданного файла копию всего вывода
use <база_данных> \u Использует заданную базу данных

Очень важная команда в этом наборе — use. Сервер mysqld предназначен для поддержки множества различных баз данных, обслуживаемых и управляемых одним серверным процессом. Во многих других серверах баз данных, таких как Oracle и Sybase, применяется термин "схема", а в СУРБД MySQL чаще используется термин "база данных". (В обозревателе запросов (Query Browser) MySQL, к примеру, применяется термин "схема".) Каждая база данных (в терминологии MySQL) представляет собой в основном независимый набор таблиц. Это позволяет настраивать разные базы данных для различных целей и назначать разных пользователей различным базам данных, используя для эффективного управления ими один и тот же сервер баз данных. С помощью команды use можно при наличии соответствующих прав переключаться между различными базами данных.

Особая база данных mysql, создаваемая автоматически при каждой установке СУРБД MySQL, применяется как основное хранилище сведений о пользователях и правах доступа.

Примечание

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

mysqladmin

Эта утилита — основное средство быстрого администрирования базы данных MySQL. В дополнение к обычным параметрам она поддерживает основные команды, перечисленные в табл. 8.4.

Таблица 8.4

Команда Описание
create <база_данных> Создает новую базу данных
drop <база_данных> Удаляет базу данных
password <новый_пароль> Изменяет пароль (как вы уже видели)
ping Проверяет, работает ли сервер
reload Повторно загружает таблицы полномочий, управляющие правами доступа
status Предоставляет сведения о состоянии сервера
shutdown Выключает сервер
variables Отображает переменные, управляющие работой MySQL, и их текущие значения
version Выводит номер версии сервера и время его работы

Запустите из строки приглашения утилиту mysqladmin без параметров, чтобы увидеть полный список опций. Вам потребуется применить | less.

mysqlbug

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

mysqldump

Это крайне полезная утилита, позволяющая получить частичный или полный дамп базы данных в виде единого файла с набором команд языка SQL, которые могут быть считаны обратно в MySQL или в другую СУРБД. Она принимает как параметр стандартную информацию о пользователе и пароль, а также имена базы данных и таблиц. Дополнительные опции, приведенные в табл. 8.5, существенно расширяют функциональные возможности этой утилиты.

Таблица 8.5

Команда Описание
--add-drop-table Вставляет в файл вывода операторы SQL для удаления любых таблиц перед командой их создания
-e Применяет расширенный синтаксис вставки. Это нестандартный язык SQL, но если вы получаете дамп больших объемов информации, это поможет гораздо быстрее повторно загрузить дамп вашей базы в СУРБД MySQL
-t Получает дамп только данных из таблиц, а не информации, необходимой для создания таблиц
-d Получает дамп только структуры таблиц, а не реальных данных

По умолчанию mysqldump посылает эти данные в стандартный вывод, поэтому вам потребуется перенаправление их в файл.

Эта утилита очень удобна для перемещения данных или быстрого создания резервных копий, и благодаря клиент-серверной реализации MySQL ее даже можно использовать для выполнения сложного удаленного резервного копирования с помощью клиента mysqldump, установленного на другой машине. Далее для примера приведена команда подключения пользователя rick и получения дампа базы данных myplaydb:

$ mysqldump -u rick -p myplaydb > myplaydb.dump

Результирующий файл, у которого в нашей системе только одна таблица в базе данных, выглядит следующим образом:

-- MySQL dump 10.11

--

-- Host: localhost Database: myplaydb

-- --------------------------------------------------

-- Server version 5.0.37

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;

/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;

/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;

/*!40101 SET NAMES utf8 */;

/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;

/*!40103 SET TIME_ZONE='+00:00' */;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;

/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0*/;

/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'*/;

/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--

-- Table structure for table 'children'

--

DROP TABLE IF EXISTS 'children';

CREATE TABLE 'children' (

 'childno' int(11) NOT NULL auto_increment,

 'fname' varchar(30) default NULL,

 'age' int(11) default NULL,

 PRIMARY KEY ('childno')

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

--

-- Dumping data for table 'children'

--

LOCK TABLES 'children' WRITE;

/*!40000 ALTER TABLE 'children'DISABLE KEYS */;

INSERT INTO 'children' VALUES

(1,'Jenny',21),(2,'Andrew',17),(3,'Gavin',8), (4,'Duncan',6),(5,'Emma',4),

(6,'Alex',15),(7,'Adrian',9);

/*!40000 ALTER TABLE 'children'ENABLE KEYS */;

UNLOCK TABLES;

/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;

/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;

/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;

/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2007-0.6-22 20:11:48

mysqlimport

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

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

mysqlshow

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

□ Без параметров она отображает все имеющиеся базы данных.

□ С базой данных в качестве параметра она выводит таблицы этой базы данных.

□ С именами базы данных и таблицы утилита отображает перечень столбцов заданной таблицы.

□ Если заданы база данных, таблица и столбец, утилита выводит подробную информацию о заданном столбце.

Создание пользователей и наделение их правами доступа

В роли администратора MySQL вам чаще всего придется обслуживать пользователей: добавлять, и удалять пользователей СУРБД MySQL и управлять их полномочиями. Начиная с версии MySQL 3.22, правами доступа или полномочиями пользователей управляют в мониторе MySQL с помощью команд grant и revoke — задача, гораздо менее устрашающая, чем непосредственная корректировка таблиц прав доступа, которая требовалась в ранних версиях MySQL.

grant

Команда MySQL grant почти, хотя и не полностью, соответствует синтаксису стандарта SQL92. Далее приведен общий формат:

grant <привилегия> on <объект> to <пользователь> [identified by user-password] [with grant option];

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

Таблица 8.6

Значение Описание
alter Изменять таблицы и индексы
create Создавать базы данных и таблицы
delete Удалять данные из базы данных.
drop Удалять базы данных и таблицы
index Управлять индексами
insert Вставлять данные в базу данных
lock tables Разрешает блокировать таблицы
select Извлекать данные
update Изменять данные
all Все вышеперечисленные

У некоторых прав доступа есть дополнительные опции. Например, create view дает пользователю право создавать представления. Для получения полного списка прав доступа обратитесь к документации MySQL, относящейся к вашей версии СУРБД, поскольку эта область расширяется с каждой новой версией MySQL. Существует также несколько специальных административных прав доступа, но здесь мы их не рассматриваем.

Объект, которому вы предоставляете данные права, обозначается как

databasename.tablename

и в лучших традициях Linux * — ссылка на любое имя, поэтому *.* означает все объекты в каждой базе данных, a foo.* — все таблицы в базе данных foo.

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

В синтаксисе языка SQL специальный символ % — символ подстановки, во многом сходный, с символом * в среде командной оболочки. Вы можете формировать отдельные команды для каждого требуемого набора прав доступа, но если, например, вы хотите предоставить доступ пользователю rick с любого компьютера в домене wiley.com, пользователя rick можно описать как

rick@'%.wiley.com'

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

Вы также можете применять нотацию IP/Netmask (N.N.N.N/M.M.M.M), задающую сетевой адрес для управления доступом.

Также, как раньше вы использовали описание rick@'192.163.0.0/255.255.255.0' для предоставления пользователю rick доступа с любого сетевого компьютера, можно задать rick@'192.168.0.1' для ограничения доступа пользователя rick единственной рабочей станцией или ввести rick@'192.0.0.0/255.0.0.0', расширив область действия прав до любой машины в сети 192 класса А.

В еще одном примере команда

mysql> GRANT ALL ON foo.* TO rick@'%' IDENTIFIED BY 'bar';

создает пользователя rick с полным набором прав доступа к базе данных foo для подключения с любой машины с начальным паролем bar.

Если базы данных foo до сих пор не существует, у пользователя rick теперь появится право создать ее с помощью команды SQL create database.

Ключевые слова IDENTIFIED BY — не обязательная часть команды, но убедиться в том, что у всех пользователей во время их создания появляются пароли, совсем неплохо.

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

Обычно ключевые слова with grant option применяются только для создания вспомогательного административного пользователя, но они также могут использоваться для разрешения вновь созданному пользователю получить права доступа, предоставляемые ему другими пользователями. Всегда применяйте ключевые слова with grant option обдуманно.

revoke

Естественно, администратор не может только предоставлять права, но также и лишает прав. Делается это с помощью команды revoke.

revoke <привилегия> on <объект> from <пользователь>

и с применением почти такого же формата, как в команде grant. Например:

mysql> REVOKE INSERT ON foo.* FROM rick@'%';

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

mysql> use mysql

mysql> DELETE FROM user WHERE user = "rick"

mysql> FLUSH PRIVILEGES;

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

Примечание

Имейте в виду, что команда delete не относится к группе команд grant и revoke. Синтаксис SQL делает ее применение необходимым из-за способа обработки прав доступа в MySQL. Вы напрямую обновляете таблицы прав доступа MySQL (поэтому первой применяется команда use mysql) для внесения нужных вам изменений эффективным способом.

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

Пароли

Если вы хотите задать пароли для уже существующих пользователей, не имевших их до сих пор, или изменить собственный или чей-то пароль, следует подключиться к серверу MySQL как пользователь root и напрямую обновить данные пользователя. Например,

mysql> use mysql

mysql> SELECT host, user, password FROM user;

Вы должны получить перечень, похожий на следующий:

+-----------+------+------------------+

| host      | user | password         |

+-----------+------+------------------+

| localhost | root | 67457e226a1a15bd |

| localhost | foo  |                  |

+-----------+------+------------------+

2 rows in set (0.00 sec)

Если вы хотите присвоить пароль bar пользователю foo, можно сделать следующее:

mysql> UPDATE user SET password = password('bar') WHERE user = 'foo';

Для проверки выведите снова соответствующие столбцы таблицы пользователей user:

mysql> SELECT host, user, password FROM user;

+-----------+------+------------------+

| host      | user | password         |

+-----------+------+------------------+

| localhost | root | 65457e236glalwbq |

| localhost | foo  | 7c9e0a41222752fa |

+-----------+------+------------------+

2 rows in set (0.00 sec) mysql>

Теперь наверняка у пользователя foo есть пароль. Не забудьте вернуться в свою исходную базу данных.

Начиная с версии MySQL 4.1, схема формирования паролей обновлена по сравнению с более ранними версиями. Но для обратной совместимости вы все еще можете задавать пароль, применяя старый алгоритм с функцией OLD_PASSWORD('password to set'), если вам это нужно.

Создание базы данных

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

mysql> GRANT ALL ON *.* TO rick@localhost IDENTIFIED BY 'secretpassword';

Теперь протестируйте набор прав доступа, зарегистрировавшись как rick, и создайте базу данных:

$ mysql -u rick -р

Enter password:

...

mysql> CREATE DATABASE rick;

Query OK, 1 row affected (0.01 sec).

mysql>

Далее сообщите MySQL о том, что вы хотите переключиться на вашу новую базу данных:

mysql> use rick

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

$ mysql -u rick -p rick

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

Типы данных

Итак, у вас есть действующий сервер MySQL, безопасная регистрация вашего пользователя и база данных, готовая к применению. Что дальше? Сейчас нужно создать несколько таблиц со столбцами для хранения данных. Но прежде чем вы сможете сделать это, следует узнать о типах данных, поддерживаемых MySQL.

Типы данных MySQL довольно обычны, поэтому мы лишь бегло пробежимся по основным типам, и как всегда более подробную информацию можно найти в руководстве по MySQL на Web-сайте MySQL.

Тип Boolean

Столбец логического типа можно определить с помощью ключевого слова BOOL. Как вы и ожидали, в нем могут храниться значения TRUE и FALSE, а также специальное "неопределенное" значение баз данных NULL.

Символьный тип

В табл. 8.7 перечислены все доступные символьные типы. Первые три — стандартные, оставшиеся три специфичны для MySQL. Мы полагаем, что на практике вы будете придерживаться стандартных типов.

Таблица 8.7

Определение Описание
CHAR Одиночный символ
CHAR(N) Символьная строка длиной точно N символов, которая будет при необходимости заполняться пробелами. Максимальная длина 255 символов
VARCHAR(N) Массив переменной длины из N символов. Максимальная длина 255 символов
TINYTEXT Аналогичен VARCHAR(N)
MEDIUMTEXT Текстовая строка длиной до 65 535 символов
LONGTEXT Текстовая строка длиной до 2³²–1 символов

Числовой тип

В табл. 8.8 показано, что числовые типы делятся на целочисленные и типы с плавающей точкой.

Таблица 8.8

Определение Тип Описание
TINYINT Целочисленный 8-битный тип данных
SMALLINT Целочисленный 16-битный тип данных
MEDIUMINT   24-битный тип данных
INT Целочисленный 32-битный тип данных. Это стандартный тип и хороший выбор для данных общего назначения
BIGINT Целочисленный 64-битный знаковый тип данных
FLOAT(P) С плавающей точкой Числа с плавающей точкой с точностью как минимум P знаков
DOUBLE(D, N) С плавающей точкой Числа с плавающей точкой и двойной точностью из D цифр и N десятичных знаков
NUMERIC(P, S) С плавающей точкой Действительные числа длиной P разрядов всего с S десятичными разрядами из них. В отличие от DOUBLE это точно заданное число (exact number), поэтому оно больше подходит для хранения денежных сумм, но обрабатывается менее эффективно
DECIMAL(Р, S) С плавающей точкой Синоним NUMERIC

Мы полагаем, что в основном вы будете пользоваться типами INT, DOUBLE и NUMERIC, поскольку они ближе всего к стандартным типам SQL. Остальные типы нестандартные и могут отсутствовать в тех системах управления базами данных, куда вы решите переместить данные когда-либо в будущем.

Временной тип

В табл. 8.9 перечислены пять имеющихся временны́х типов.

Таблица 8.9

Определение Описание
DATE Хранит даты с 1 января 1000 г. по 31 декабря 9999 г.
TIME Хранит время с -838:59:59 до 838:59:59
TIMESTAMP Хранит метку времени, начиная с 1 января 1970 г. и по 2037 г.
DATETIME Хранит даты с 1 января 1000 г. по последнюю секунду 31 декабря 9999 г.
YEAR Хранит номер года. Будьте осторожны с двузначными величинами, поскольку они неоднозначны и автоматически преобразуются в четырехзначные числа.

Учтите, что следует быть внимательными при сравнении значений типов DATE и DATETIME в отношении способа обработки значения времени; результаты могут оказаться неожиданными. Подробности ищите в руководстве по MySQL, поскольку поведение разных версий СУРБД слегка отличается.

Создание таблицы

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

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

База данных может, если для этого есть разумные основания, содержать очень много, практически неограниченное количество таблиц. Однако лишь немногим СУРБД требуется более 100 таблиц, а большинству маленьких систем вполне достаточно 25 или около того таблиц.

Синтаксис языка SQL, посвященный созданию объектов баз данных и называемый DDL (data definition language, язык определения данных), невозможно охватить полностью в одной главе; все подробности есть в разделе документации, на Web-сайте MySQL.

Базовый синтаксис для создания таблиц следующий:

CREATE TABLE <таблица> {

 column type [NULL | NOT | NULL] [AUTO_INCREMENT] [PRIMARY KEY]

 [, ...]

 [, PRIMARY KEY (столбец [, ...] ) ]

)

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

DROP TABLE <таблица>

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

Таблица 8.10

Ключевое слово Описание
AUTO INCREMENT Это специальное ключевое слово сообщает MySQL о том, что, когда вы пишете в данный столбец NULL, следует автоматически заполнить столбец данными с помощью автоматически формируемого числа с наращением. Это чрезвычайно полезное средство; оно позволяет применять MySQL для автоматического назначения уникальных номеров строкам ваших таблиц, хотя оно может применяться только в столбцах, являющихся также первичными ключами. В других системах управления базами данных оно часто реализуется порядковым типом или управляется более явно с помощью последовательности
NULL Специальное значение в базе данных, обычно применяемое для обозначения "неизвестной" величины, но может также использоваться для обозначения "неподходящего" значения. Например, если вы заполняете таблицу подробными данными о сотрудниках, у вас может быть столбец с адресом электронной почты. В этом случае вы будете хранить NULL вместо адреса данного сотрудника, чтобы показать, что для конкретного человека эта информация не известна. Запись NOT NULL означает, что в этом столбце нельзя хранить значения NULL и может оказаться полезной для того, чтобы помешать вводу в такие столбцы значений NULL, если, например, значение всегда должно быть известно, как в случае фамилии сотрудника
PRIMARY KEY Указывает на то, что данные в этом столбце будут уникальными и разными во всех строках данной таблицы. У каждой таблицы может быть только один первичный ключ

Выполните упражнение 8.1.

Упражнение 8.1. Создание таблицы и вставка данных

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

1. Вам нужен следующий оператор языка SQL

CREATE TABLE children (

 childno INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY,

 fname VARCHAR(30),

 age INTEGER

);

Примечание

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

2. Вы также можете определить первичный ключ отдельно в определении столбца. Далее приведен пример интерактивного сеанса, в котором показан альтернативный синтаксис:

mysql> use rick

Database changed

mysql> CREATE table children (

    -> childno INTEGER AUTO_INCREMENT NOT NULL,

 -> fname varchar(30),

 -> age INTEGER,

 -> PRIMARY KEY(childno)

 -> );

Query OK, 0 rows affected (0.04 sec)

mysql>

Вы можете записать команду или оператор SQL в нескольких строках, и монитор mysql применит подсказку ->, чтобы показать, что вы находитесь в строке продолжения. Как упоминалось ранее, команда SQL завершается точкой с запятой, чтобы показать, что вы закончили и готовы к обработке вашего запроса сервером базы данных.

Если вы допустили ошибку, MySQL разрешит вернуться назад к предыдущим командам, откорректировать и повторно ввести их простым нажатием клавиши <Enter>.

3. Теперь у вас есть таблица, в которую можно вводить данные. Данные добавляются с помощью SQL-команды INSERT. Поскольку вы определили столбец childno как AUTO_INCREMENT, в него не вводятся данные, вы просто разрешаете MySQL разместить в нем уникальный номер.

mysql> INSERT INTO children(fname, age) VALUES("Jenny", 21);

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO children(fname, age) VALUES("Andrew", 17);

Query OK, 1 row affected (0.00 sec)

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

mysql> SELECT childno, fname, age FROM children;

+---------+--------+-----+

| childno | fname  | age |

+---------+--------+-----+

| 1       | Jenny  | 21  |

| 2       | Andrew | 17  |

2 rows in set (0.00 sec) mysql>

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

Как это работает

Вы открыли интерактивный сеанс подключения к серверу базы данных и переключились на базу данных rick. Затем вы ввели команду SQL для создания вашей таблицы, используя нужное количество строк для ввода команды. Как только вы завершили команду с помощью знака ;, MySQL создала вашу таблицу. Затем вы применили команду INSERT для ввода данных в вашу новую таблицу, позволив в столбце childno автоматически размещать числа. В заключение вы применили команду SELECT для вывода данных вашей таблицы.

Объем данной главы не позволяет дать полное описание языка SQL и тем более принципов проектирования баз данных. Дополнительную информацию см. на Web-сайте www.mysql.com.

Графические средства

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

У СУРБД MySQL два основных графических средства: MySQL Administrator и MySQL Query Browser. Точное имя пакета с этими средствами зависит от используемого вами дистрибутива; например, в дистрибутивах Red Hat ищите mysql-gui-tools и mysql-administrator. В дистрибутиве Ubuntu вам, возможно, сначала придется переключиться на универсальный ("Universe") репозитарий, а затем искать mysql-admin.

MySQL Query Browser

Обозреватель запросов (query browser) довольно простое, но эффективное средство. После установки его можно запустить из меню GUI (graphical user interface, графический интерфейс пользователя). Запустив обозреватель, вы увидите начальный экран, запрашивающий подробности подключения (рис. 8.4).

Рис. 8.4

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

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

Рис. 8.5

MySQL Administrator

Мы очень надеемся на то, что вы познакомитесь с MySQL Administrator. Это мощный, стабильный и легкий в использовании графический интерфейс для СУРБД MySQL, заранее откомпилированная версия которого существует как для ОС Linux, так и для Windows (даже исходный код доступен, если он вам нужен). MySQL Administrator позволяет управлять сервером MySQL и выполнять команды SQL через графический интерфейс пользователя.

При запуске MySQL Administrator выводится экран подключения, очень похожий на экран подключения MySQL Query Browser. После ввода некоторых подробностей у вас появится главная страница управления (рис. 8.6).

Рис. 8.6

Если вы хотите управлять сервером MySQL из программы-клиента в ОС Windows, можно загрузить Windows-версию MySQL Administrator из раздела Web-сайта MySQL, посвященного средствам GUI. Когда писалась эта книга, в загрузку были включены администратор, обозреватель запросов и утилита переноса базы данных. На рис. 8.7 показан экран состояния, как видите, он почти идентичен версии Linux,

Примечание

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

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

Рис. 8.7 

Доступ к данным MySQL из программ на С

Теперь, когда основы СУРБД MySQL остались в стороне, давайте рассмотрим, как, не применяя графические средства или программу-клиент mysql, получить доступ к СУРБД MySQL из вашего приложения.

К MySQL можно получить доступ из программ на разных языках, включая следующие:

□ С;

□ Eiffel;

□ С++;

□ Tcl;

□ Java;

□ Ruby;

□ Perl;

□ Python;

□ PHP.

Есть и драйвер ODBC для доступа к MySQL из приложений ОС Windows, таких как Access. Существует даже драйвер ODBC для ОС Linux, но в его применении мало смысла.

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

Подпрограммы подключения

Подключение к базе данных MySQL из программы на языке С состоит из двух шагов:

□ инициализации структуры идентификации подключения или дескриптора подключения;

□ выполнения физического подключения.

Сначала примените mysql_init для инициализации дескриптора вашего подключения:

#include <mysql.h>

MYSQL *mysql_init(MYSQL*);

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

К этому моменту вы просто выделили память и инициализировали структуру дескриптора. Но вы еще должны предложить параметры подключения с помощью подпрограммы mysql_real_connect:

MYSQL *mysql_real_connect(MYSQL *connection,

 const char *server host, const char *sql_user_name,

 const char *sql_password, const char *db_name,

 unsigned int port_number, const char *unix_socket_name,

 unsigned int flags);

Указатель подключения должен указывать на структуру дескриптора, уже инициализированную подпрограммой mysql_init. Параметры в большинстве своем очевидны; но следует отметить, что server_host может задаваться именем компьютера или IP-адресом. При подключении только к локальной машине вы можете оптимизировать тип подключения, указав в качестве этого параметра localhost.

Параметры sql_user_name и sql_password соответствуют своим именам. Если регистрационное имя равно NULL, предполагается идентификатор текущего пользователя ОС Linux. Если пароль — NULL, вы сможете обратиться к данным только на том сервере, который доступен без пароля. Перед отправкой по сети пароль шифруется.

Параметры port_number и unix_socket_name должны быть равны 0 и NULL соответственно, если вы не меняли стандартных настроек в вашей установке MySQL. Эти параметры примут соответствующие значения по умолчанию.

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

Если подключиться невозможно, возвращается NULL. В этом случае полезную информацию может предоставить подпрограмма mysql_error.

Когда вы прекращаете использовать подключение, обычно при завершении программы, вызовите подпрограмму mysql_close, как показано далее:

void mysql_close(MYSQL * connection);

Эта подпрограмма разорвет соединение с сервером. Если подключение устанавливалось с помощью mysql_init, память, отведенная под структуру дескриптора, освободится. Указатель станет неопределенным, и его нельзя будет применять в дальнейшем. Открытое ненужное подключение означает расточительное использование ресурсов, но повторное открытие подключения сопряжено с дополнительными накладными расходами, поэтому решайте сами, когда применять описанные подпрограммы.

Подпрограмма mysql_options (которую можно вызвать только между вызовами mysql_init и mysql_real_connect) позволит настроить некоторые параметры.

int mysql_options(MYSQL* connection, enum option_to_set, const char *argument);

Поскольку при каждом вызове mysql_options способна настроить только один параметр, ее следует вызывать отдельно для каждого параметра, который нужно задать. Вы можете применять эту подпрограмму необходимое количество раз, но все вызовы должны находиться между вызовами подпрограмм mysql_init и mysql_real_connect. Не все параметры подпрограммы имеют тип char, который следует приводить как const char*. Три самых часто используемых параметра приведены в табл. 8.11. И как всегда в расширенном интерактивном руководстве приведен полный список параметров.

Таблица 8.11

Enum-параметр Действительный тип аргумента Описание
MYSQL_ОРТ_CONNECT_TIMEOUT const unsigned int* Количество секунд ожидания перед закрытием подключения из-за простоя
MYSQL_ОРТ_COMPRESS Нет, используйте NULL Применять сжатие при сетевом подключении
MYSQL_INIT_COMMAND const char* Команда, отправляемая при каждом установлении подключения

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

Для задания времени ожидания, равного семи секундам, используйте следующий фрагмент программного кода:

unsigned int timeout = 7;

...

connection = mysql_init(NULL);

ret = mysql_options(connection, MYSQL_OPT_CONNECT_TIMEOUT, (const char *)&timeout);

if (ret) {

 /* Обработка ошибки */

 ...

}

connection = mysql_real_connect(connection ...)

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

Начните с задания нового пароля для пользователя (в приведенном далее коде rick на localhost) и затем создайте базу данных foo, к которой будете подключаться. Вы все это уже знаете, поэтому мы просто приводим последовательность действий:

$ mysql -u root -р

Enter password:

Welcome to the MySQL monitor. Commands end with ; or \g.

mysql> GRANT ALL ON *.* TO rick@localhost IDENTIFIED BY 'secret';

Query OK, 0 rows affected (0.01 sec)

mysql> \q

Bye

$ mysql -u rick -p

Enter password:

Welcome to the MySQL monitor. Commands end with ; or \g.

mysql> CREATE DATABASE foo;

Query OK, 1 row affected (0.01 sec)

mysql> \q

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

Далее приведен файл create_children.sql:

--

-- Create the table children

--

CREATE TABLE children (

 childno int(11) NOT NULL auto_increment,

 fname varchar(30),

 age int(11),

 PRIMARY KEY (childno)

);

--

--Populate the table 'children'

--

INSERT INTO children(childno, fname, age) VALUES (1,'Jenny',21);

INSERT INTO children(childno, fname, age) VALUES (2,'Andrew',17);

INSERT INTO children(childno, fname, age) VALUES (3,'Gavin',8);

INSERT INTO children(childno, fname, age) VALUES (4,'Duncan', 6);

INSERT INTO children(childno, fname, age) VALUES (5,'Emma',4);

INSERT INTO children(childno, fname, age) VALUES (6,'Alex',15);

INSERT INTO children(childno, fname, age) VALUES (7,'Adrian',9);

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

$ mysql -u rick --password=secret foo

Welcome to the MySQL monitor. Commands end with ; or \g.

mysql> \. create_children.sql

Query OK, 0 rows affected (0.01 sec)

Query OK, 1 row affected (0.00 sec)

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

Далее приведен файл connect1.с, который подключается от имени пользователя rick с паролем secret к серверу на локальной машине и базе данных foo.

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

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

 MYSQL *conn_ptr;

 conn_ptr = mysqlinit(NULL);

 if (!conn_ptr) {

  fprintf(stderr, "mysql_init failed\n");

  return EXIT_FAILURE;

 }

 conn_ptr = mysql_real_connect(conn_ptr, "localhost", "rick", "secret",

  "foo", 0, NULL, 0);

 if (conn_ptr) {

  printf("Connection success\n");

 } else {

  printf ("Connection failed\n");

 }

 mysql_close(conn_ptr);

 return EXIT_SUCCESS;

}

Теперь откомпилируйте программу и посмотрите, как вы это сделали. Возможно, придется вставить путь к файлам include и путь к библиотекам, а также указать, что файл нуждается в компоновке с библиотечным модулем mysqlclient. В некоторых системах может понадобиться опция -lz для компоновки с библиотекой упаковки (compression library). В системе авторов требуемая строка компиляции выглядит следующим образом:

$ gcc -I/usr/include/mysql connect1.с -L/usr/lib/mysql -lmysqlclient -о connect1

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

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

$ ./connect1

Connection success $

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

Как видите, подключиться к базе данных MySQL очень легко.

Обработка ошибок

Прежде чем мы перейдем к более сложным программам, полезно взглянуть на то, как MySQL обрабатывает ошибки. СУРБД MySQL использует ряд возвращаемых числовых кодов, предоставляемых дескриптором подключения. К двум обязательным подпрограммам относятся следующие:

unsigned int mysql_errno(MYSQL *connection);

и

char *mysql_error(MYSQL *connection);

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

Возвращаемое значение — в действительности код ошибки, коды ошибок определены в файле include с именем errmsg.h или в файле mysqld_error.h. Оба файла можно найти в каталоге MySQL с именем include. Первый сообщает об ошибках клиентской стороны, а второй — об ошибках сервера.

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

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

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

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

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

 MYSQL my_connection;

 mysql_init(&my_connection);

 if (mysql_real_connect(&my_connection, "localhost", "rick",

  "I do not know", "foo", 0, NULL, 0)) {

  printf("Connection success\n");

  mysql_close(&my_connection);

 } else {

  fprintf(stderr, "Connection failed\n");

  if (mysql_errno(&my_connection)) {

   fprintf(stderr, "Connection error %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection));

  }

 }

 return EXIT_SUCCESS;

}

Вы смогли легко решить проблему, устранив перезапись вашего дескриптора подключения результатом, возвращаемым при аварийном завершении mysql_real_connect. И кроме того, это отличный пример еще одного способа применения структур дескрипторов подключения. Вы можете вызвать ошибку, выбрав некорректное имя пользователя или пароль, и получите код ошибки, подобный предлагаемому монитором mysql:

$ ./connect2

Connection failed

Connection error 1045: Access denied for user: 'rick@localhost' (Using password: YES)

$

Выполнение SQL-операторов

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

int mysql_query(MYSQL *connection, const char *query);

He слишком сложная? Эта подпрограмма принимает указатель на дескриптор подключения и несколько, хочется надеяться, корректных SQL-операторов в виде текстовой строки (без завершения каждого из них точкой с запятой, как в мониторе mysql). В случае удачного завершения возвращается ноль. Вторую подпрограмму mysql_real_query можно применять при запросе двоичных данных, но в этой главе мы используем только подпрограмму mysql_query.

SQL-операторы, не возвращающие данных

Для простоты начнем с рассмотрения нескольких SQL-операторов, которые не возвращают данные: UPDATE, DELETE и INSERT.

Еще одна важная функция, которую мы рассмотрим, проверяет количество строк, затронутых запросом:

my_ulonglong mysql_affected_rows(MYSQL *connection);

Первое, что вы, вероятно, заметили в этой функции, — очень необычный тип возвращаемых данных. Из соображений переносимости применяется беззнаковый (unsigned) тип. Когда используется функция printf, рекомендуется приводить его к типу unsigned long (длинное беззнаковое) со спецификатором формата %lu. Эта функция возвращает количество строк, измененных предварительно выполненным запросом UPDATE, INSERT или DELETE. Возвращаемое значение, используемое в MySQL, может вас, озадачить, если у вас есть опыт работы с другими базами данных SQL. СУРБД MySQL возвращает количество строк, действительно измененных обновлением, в то время как многие другие СУБД будут считать запись измененной просто потому, что она соответствует одному из условий WHERE.

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

Сначала следует создать таблицу children в вашей базе данных foo, если вы еще не сделали этого. Удалите (с помощью команды drop) любую существующую таблицу, чтобы быть уверенным в том, что вы имеете дело с чистым определением таблицы, и повторно отправьте идентификаторы, применяемые в столбце AUTO_INCREMENT.

$ mysql -u rick -p foo

Enter password:

Welcome to the MySQL monitor. Commands end with ; or \g.

mysql> DROP TABLE children;

Query OK, 0 rows affected (0.58 sec)

mysql> CREATE TABLE children (

    -> childno int(11) AUTO_INCREMENT NOT NULL PRIMARY KEY,

    -> fname varchar(30),

    -> age int

    -> );

Query OK, 0 rows affected (0.09 sec)

mysql>

Теперь добавьте программный код в файл connect2.c, для того чтобы вставить новую строку в вашу таблицу. Назовите эту новую программу insert1.с. Учтите, что разбиение оператора на несколько строк объясняется физической шириной страницы; обычно вы не должны разбивать реальный SQL-оператор, если он не слишком длинный, в этом случае можно применить символ / в конце строки для переноса оставшейся части SQL-оператора на следующую строку.

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

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

 MYSQL my_connection;

 int res;

 mysql_init(&my_connection);

 if (mysql_real_connect(&my_connection, "localhost",

  "rick", "secret", "foo", 0, NULL, 0)) {

  printf("Connection success\n");

  res = mysql_query(&my_connection,

   "INSERT INTO children(fname, age) VALUES('Ann', 3)");

  if (!res) {

   printf("Inserted %lu rows\n",

    (unsigned long)mysql_affected_rows(&my_connection));

  } else {

   fprintf(stderr, "Insert error %d: %s\n",

    mysql_errno(&my_connection), &mysql_error(&my_connection));

  }

  mysql_close(&my_connection);

 } else {

  fprintf(stderr, "Connection failed\n");

  if (mysql_errno(&my_connection)) {

   printf(stderr, "Connection error %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection));

  }

 }

 return EXIT_SUCCESS;

}

Как и ожидалось, одна строка добавлена.

Теперь измените код, чтобы включить UPDATE вместо INSERT, и посмотрите на сообщение об измененных строках.

  mysql_errno(&my_connection), mysql_error(&my_connection));

 }

}

res = mysql_query(&my_connection,

 "UPDATE children SET AGE = 4 WHERE fname = 'Ann'");

if (!res) {

 printf("Updated %lu rows\n",

  (unsigned long)mysql_affected_rows(&my_connection));

} else {

 fprintf (stderr, "Update error %d: %s\n",

  mysql_errno(&my_connection), mysql_error(&my_connection));

}

Назовите эту программу update1.c. Она пытается задать возраст 4 года для всех детей с именем Ann.

Предположим, что ваша таблица children содержит следующие данные:

mysql> SELECT * from CHILDREN;

+---------+--------+-----+

| childno | fname  | age |

+---------+--------+-----+

|       1 | Jenny  |  21 |

|       2 | Andrew |  17 |

|       3 |  Gavin |   9 |

|       4 | Duncan |   6 |

|       5 |   Emma |   4 |

|       6 |   Alex |  15 |

|       7 | Adrian |   9 |

|       8 |    Ann |   3 |

|       9 |    Ann |   4 |

|      10 |    Ann |   3 |

|      11 |    Ann |   4 |

+---------+--------+-----+

11 rows in set (0.00 sec)

В вашей таблице есть четыре ребенка с именем Ann. Вы можете рассчитывать на то, что при выполнении программы update1 количество измененных строк будет равно четырем, т.е. числу строк, отбираемых по условию WHERE. Но если вы выполните программу, то увидите отчет программы об изменении только двух строк, поскольку учитываются только те строки, данные которых действительно нуждались в корректировке. Можно выбрать более традиционный вариант отчета, используя флаг CLIENT_FOUND_ROWS в функции mysql_real_connect:

if (mysql_real_connect(&my_connection, "localhost",

 "rick", "secret", "foo", 0, NULL, CLIENT_FOUND_ROWS)) {

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

Последняя странность функции mysql_affected_rows проявляется при удалении информации из базы данных. Если вы удаляете данные с помощью условия WHERE, mysql_affected_rows вернет ожидаемое вами количество удаленных строк. Но если в операторе DELETE нет условия WHERE, будут удалены все строки, но в сообщении программы о количестве строк, затронутых запросом, будет указан ноль. Это происходит потому, что MySQL оптимизирует удаление всех строк, заменяя многократные построчные удаления.

На подобное поведение не влияет флаг CLIENT_FOUND_ROWS.

Что же вы вставили?

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

Рассмотрим определение таблицы еще раз:

CREATE TABLE children (

 childno INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY,

 fname VARCHAR(30),

 age INTEGER

);

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

Можно выполнить оператор SELECT для того чтобы извлечь данные, отобранные по имени ребенка. Но это очень неэффективный способ и не гарантирующий уникальности выбора: допустим, что у вас есть два ребенка с одним и тем же именем. Или несколько пользователей могли быстро вставить данные, и появились другие добавленные строки между вашим оператором обновления и оператором SELECT. Поскольку выяснение значения столбца типа AUTO_INCREMENT — столь распространенная проблема, MySQL предлагает специальное решение в виде функции LAST_INSERT_ID().

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

Выполните упражнение 8.2.

Упражнение 8.2. Извлечение ID, сгенерированного в столбце типа AUTO_INCREMENT

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

mysql> INSERT INTO children(fname, age) VALUES('Tom', 13);

Query OK, 1 row affected (0.06 sec)

mysql> SELECT LAST_INSERT_ID();

+------------------+

| last_insert_id() |

+------------------+

|               14 |

+------------------+

1 row in set (0.01 sec)

mysql> INSERT INTO children(fname, age) VALUES('Harry', 17);

Query OK, 1 row affected (0.02 sec)

mysql> SELECT LAST_INSERT_ID();

+------------------+

| last_insert_id() |

+------------------+

|               15 |

+------------------+

1 row in set (0.00 sec)

mysql>

Как это работает

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

Если хотите поэкспериментировать, чтобы убедиться в уникальности возвращаемого номера в вашем сеансе, откройте еще один сеанс и вставьте другую строку. В исходном сеансе повторите выполнение оператора SELECT LAST_INSERT_ID();. Вы увидите, что номер не изменился, поскольку возвращаемый номер — последний, добавленный в текущем сеансе. Но если вы выполните оператор SELECT * FROM children, то увидите, что в другом сеансе действительно были вставлены данные.

Выполните упражнение 8.3.

Упражнение 8.3. Использование автоматически формируемых ID в программе на С

В этом примере вы измените вашу программу insert1.c, чтобы посмотреть, как она работает на С. Ключевые изменения выделены цветом. Назовите откорректированную программу insert2.c.

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

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

 MYSQL my_connection;

 MYSQL_RES* res_ptr;

 MYSQL_ROW sqlrow;

 int res;

 mysql_init(&myconnection);

 if (mysql_real_connect(&my_connection, "localhost",

  "rick", "bar", "rick", 0, NULL, 0)) {

  printf("Connection success\n");

  res = mysql_query(&my_connection,

   "INSERT INTO children(fname, age) VALUES('Robert', 7)");

  if (!res) {

   printf("Inserted %lu rows\n",

    (unsigned long)mysql_affected_rows(&my_connection));

  } else {

   fprintf(stderr, "Insert error %d: %s\n",

    mysql_errno(&myconnection), mysql_error(&my_connection));

  }

  res = mysql_query(&my_connection, "SELECT LAST INSERT ID()");

  if (res) {

   printf("SELECT error %s\n", mysql_error(&my_connection);

  } else {

   res_ptr= mysql_use_result(&my_connection);

   if (res_ptr) {

    while ((sqlrow = mysql_fetch_row(res_ptr))) {

     printf("We inserted childno %s\n", sqlrow[0]);

    }

    mysql_free_result(res_ptr);

   }

  }

  mysql_close(&my_connection);

 } else {

  fprintf(stderr, "Connection failed\n");

  if (mysql_errno(&my_connection)) {

   fprintf(stderr, "Connection error %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection));

  }

 }

 return EXIT_SUCCESS;

}

Далее приведен вывод:

$ gcc -I/usr/include/mysql insert2.c -L/usr/lib/mysql -lmysqlclient -o insert2

$ ./insert2

Connection success

Inserted 1 rows

We inserted childno 6

$ ./insert2

Connection success

Inserted 1 rows

We inserted childno 7

Как это работает

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

Операторы, возвращающие данные

Основное назначение языка — конечно, извлечение данных, а не их добавление или обновление. Данные извлекаются с помощью оператора SELECT.

Примечание

MySQL также поддерживает SQL-операторы SHOW, DESCRIBE и EXPLAIN, предназначенные для возврата результатов, но мы не собираемся рассматривать их в данной книге. Как обычно, в руководстве можно найти описание этих операторов.

Получение данных в вашем приложении на языке С обычно будет включать четыре шага:

1. Выполнение запроса.

2. Извлечение данных.

3. Обработка этих данных.

4. Наведение порядка при необходимости.

Так же, как в случае операторов INSERT и DELETE, вы воспользуетесь функцией mysql_query для отправки SQL-запроса. Далее вы извлечете данные о помощью функций mysql_store_result или mysql_use_result в зависимости от того, как хотите получить данные. Затем будет применена последовательность вызовов функции mysql_fetch_row для обработки данных. И наконец, вы используете функцию mysql_free_result для очистки памяти, которая применялась для выполнения вашего запроса.

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

Функции для извлечения всех данных сразу

Вы сможете извлечь в единственном вызове все данные из оператора SELECT (или другого оператора, возвращающего данные), применяя функцию mysql_store_result:

MYSQL_RES *mysql_store_result(MYSQL* connection);

Ясно, что вам понадобится эта функция после успешного вызова функции mysql_query. Она немедленно сохранит все возвращенные данные в клиентской части. Функция вернет указатель на новую структуру, называемую структурой результирующего набора, или NULL, если оператор завершился аварийно.

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

my_ulonglong mysql_num_rows(MYSQL_RES* result);

Эта функция принимает в качестве параметра структуру с результатом, возвращенную mysql_store_result, и возвращает количество строк в данном результирующем наборе. Если функция mysql_store_result завершилась успешно, функция mysql_num_rows также завершится успешно.

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

Если окажется, что вы должны работать с особенно большим набором данных, лучше извлекать меньшие по объему, более управляемые порции данных. В этом случае приложению быстрее передается управление, и использование сетевых ресурсов будет более щадящим. Мы рассмотрим этот вариант подробнее при обсуждении функции mysql_use_result.

Теперь, когда у вас есть данные, можно обработать их с помощью функции mysql_fetch_row и перемещаться по набору, используя функции mysql_data_seek, mysql_row_seek и mysql_row_tell. Давайте рассмотрим их.

□ Функция mysql_fetch_row извлекает одну строку из структуры типа result, которую вы получили с помощью функции mysql_store_result, и помещает ее структуру row. Когда данные иссякли или возникла ошибка, возвращается NULL. В следующем разделе мы вернемся к обработке данных в структуре типа row.

MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);

□ Функция mysql_data_seek позволяет перемещаться в результирующем наборе, задавая строку, которая будет возвращена при следующем вызове функции mysql_fetch_row. Значение offset — номер строки в диапазоне от нуля до общего количества строк в результирующем наборе, уменьшенного на единицу. Передача нулевого значения вызовет возврат первой строки при следующем вызове функции mysql_fetch_row.

void mysql_data_seek(MYSQL_RES *result, my_ulonglong offset);

□ Функция mysql_row_tell возвращает величину смещения, обозначая текущую позицию в результирующем наборе. Это не номер строки и его нельзя использовать в функции mysql_data_seek.

MSSQL_ROW_OFFSET mysql_row_tell(MYSQL_RES *result);

Но ее можно применять с функцией

MYSQL_ROW_OFFSET mysql_row_seek(MYSQL_RES *result,

 MYSQL_ROW_OFFSET offset);

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

Примечание

Эта пара функций очень полезна для перемещения между известными записями в результирующем наборе. Будьте внимательны и не путайте величину смещения, используемую функциями row_tell и row_seek со значением смещения, применяемым в функции data_seek. Иначе ваши результаты будут непредсказуемыми.

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

void mysql_free_result(MYSQL_RES *result);

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

Извлечение данных

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

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

MYSQL my_connection;

MYSQL_RES *res_ptr;

MYSQL_ROW sqlrow;

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

 int res;

 mysql_init(&my_connection);

 if (mysql_real_connect(&my_connection, "localhost", "rick",

  "secret", "foo", 0, NULL, 0)) {

  printf("Connection success\n");

  res = mysql_query(&my_connection,

   "SELECT childno, fname, age FROM children WHERE age > 5");

  if (res) {

   printf("SELECT error: %s\n", mysql_error(&my_connection));

  } else {

   res_ptr = mysql_store_result(&my_connection);

   if (res_ptr) {

    printf("Retrieved %lu rows\n",

     (unsigned long)mysql_num_rows(res_ptr));

    while ((sqlrow = mysql_fetch_row(res_ptr))) {

     printf("Fetched data...\n");

    }

    if (mysql_errno(&my_connection)) {

     fprintf(stderr, "Retrieve error: %s\n",

      mysql_error(&my_connection));

    }

    mysql_free_result(res_ptr);

   }

  }

  mysql_close(&my_connection);

 } else {

  fprintf(stderr, "Connection failed\n');

  if (mysql_errno(&my_connection)) {

   fprintf(stderr, "Connection error %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection));

  }

 }

 return EXIT_SUCCESS;

}

Построчное извлечение данных

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

MYSQL_RES *mysql_use_result(MYSQL *connection);

Как и mysql_store_result, функция mysql_use_result в случае ошибки возвращает NULL; если она выполняется успешно, то возвращает указатель на объект с результирующим набором. Но эта функция отличается тем, что не считывает никаких данных в результирующий набор, который инициализировала.

Примечание

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

В чем же выигрыш от вызова функции mysql_use_result по сравнению с вызовом функции mysql_store_result? У первой из названных функций есть ряд существенных преимуществ, касающихся управления ресурсами; но ее нельзя применять с функциями mysql_data_seek, mysql_row_seek или mysql_row_tell и польза от применения mysql_num_rows ограничена, поскольку она не может нормально функционировать до тех пор, пока не будут извлечены все данные.

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

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

Замена программы select1.c на программу select2.c, использующую метод mysql_use_result, проста, поэтому далее мы приводим измененный фрагмент в виде закрашенных серым цветом строк:

if (res) {

 printf("SELECT error: %s\n", mysql_error(&my_connection));

} else {

 res_ptr = mysql_use_result(&my_connection);

 if (res_ptr) {

  while ((sqlrow = mysql_fetch_row(res_ptr))) {

   printf("Fetched data...\n");

  }

  if (mysql_errno(&my_connection)) {

   printf("Retrieve error: %s\n", mysql_error(&my_connection));

  }

  mysql_free_result(res_ptr);

 }

}

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

Обработка полученных данных

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

MySQL, как большинство баз данных SQL, возвращает два вида данных:

□ данные, извлеченные из таблицы и называемые данными столбцов;

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

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

Функция mysql_field_count предоставляет некоторую базовую информацию о результате запроса. Она принимает ваше подключение как объект и возвращает количество полей (столбцов) в результирующем наборе.

unsigned int mysql_field_count(MYSQL * connection);

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

Примечание

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

Если оставить в стороне заботы о форматировании, вы уже знаете, как немедленно вывести данные. Добавьте простую функцию display_row в программу select2.c.

Примечание

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

1. Далее приведена очень простая подпрограмма для вывода данных:

void display_row() {

 unsigned int field_count;

 field_count = 0;

 while (field_count < mysql_field_count(&my_commection)) {

  printf("%s ", sqlrow[field_count]);

  field_count++;

 }

 printf("\n");

}

2. Вставьте ее в конец файла select2.c и добавьте объявление и вызов функции:

void display_row();

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

 int res;

 mysql_init(&my_connection);

 if (mysql_real_connect(&my_connection, "localhost", "rick",

  "bar", "rick", 0, NULL, 0)) {

  printf("Connection success\n");

  res = mysql_query(&my_connection,

   "SELECT childno, fname, age FROM children WHERE age > 5");

  if (res) {

   printf("SELECT error: %s\n", mysql_error(&my_connection));

  } else {

   res_ptr = mysql_use_result(&my_connection);

   if (res_ptr) {

    while ((sqlrow = mysql_fetch_row(res_ptr))) {

     printf("Fetched data...\n");

     display_row();

    }

   }

  }

 }

}

3. Теперь сохраните законченный проект с именем select3.c. В заключение откомпилируйте и выполните select3, как показано далее:

$ gcc -I/usr/include/mysql select3. с -L/usr/lib/mysql -lmysqlclient -о select3

$ ./select3

Connection success

Fetched data...

1 Jenny 21

Fetched data...

2 Andrew 17

$

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

MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result);

Вызывать эту функцию следует многократно, до тех пор, пока не будет возвращено значение NULL, которое сигнализирует о том, что данные закончились. Далее вы можете использовать указатель на структуру данных о поле для получения сведений о столбце. Структура типа MYSQL_FIELD определена в файле mysql.h, как показано в табл. 8.12.

Таблица 8.12

Поле в структуре типа MYSQL_FIELD Описание
char *name; Имя столбца в виде строки
char *table; Имя таблицы, из которой получен столбец. Оно особенно полезно в запросе с использованием нескольких таблиц. Имейте в виду, что вычисляемое значение в результате, такое как MAX, будет иметь пустую строку для имени таблицы
char *def; При вызове функции mysql_list_fields (которую мы не обсуждаем) это поле содержит значение в столбце по умолчанию
enum enum_field_types type; Тип столбца. См. пояснения сразу после таблицы
unsigned int length; Ширина столбца, заданная при определении таблицы
unsigned int max_length; Если применяется функция mysql_store_result, это поле содержит длину в байтах самого длинного извлеченного значения столбца. Если применяется функция mysql_use_result, поле не задается
unsigned int flags; Флаги содержат информацию об определении столбца, а не о найденных данных. у распространенных флагов очевидные значения: NOT_NULL_FLAG, PRI_KEY_FLAG, UNSIGNED_FLAG, AUTO_INCREMENT_FLAG и BINARY_FLAG. Полный список флагов можно найти в документации MySQL
unsigned int decimals; Количество знаков после десятичной точки. Справедливо только для числовых полей

Типов столбца огромное множество. Полный перечень можно найти в файле mysql_com.h и в документации.

К самым распространенным относятся следующие:

FIELD_TYPE_DECIMAL

FIELD_TYPE_LONG

FIELD_TYPE_STRING

FIELD_TYPE_VAR_STRING

Далее приведен особенно полезный макрос IS_NUM, возвращающий значение true, если тип поля числовой:

if (IS_NUM(myslq_field_ptr->type)) printf("Numeric type field\n");

Прежде чем обновлять вашу программу, следует упомянуть еще одну функцию:

MYSQL_FIELD_OFFSET mysql_field_seek(MYSQL_RES* result,

 MYSQL_FIELD_OFFSET offset);

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

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

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

#include <stdlib.h>

#include <stdio.h>

#include "mysql.h"

MYSQL my_connection;

MYSQL_RES *res_ptr;

MYSQL_ROW sqlrow;

void display_header();

void display_row();

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

 int res;

 int first_row = 1; /* Применяется для гарантии того,

                       что мы выводим заголовок строки точно один раз,

                       когда данные успешно извлечены */

 mysql_init(&my_connection);

 if (mysql_real_connect(&my_connection, "localhost", "rick",

  "secret", "foo", 0, NULL, 0)) {

  printf("Connection success\n");

  res = mysql_query(&my_connection,

   "SELECT childno, fname, age FROM children WHERE age > 5");

  if (res) {

   fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

  } else {

   res_ptr = mysql_use_result(&my_connection);

   if (res_ptr) {

    while ((sqlrow = mysql_fetch_row(res_ptr))) {

     if (first_row) {

      display_header();

      first_row = 0;

     }

     display_row();

    }

    if (mysql_errno(&my_connection)) {

     fprintf(stderr, "Retrieve error: %s\n", mysql_error(&my_connection));

    }

    mysql_free_result(res_ptr);

   }

  }

  mysql_close(&my_connection);

 } else {

  fprintf(stderr, "Connection failed\n");

  if (mysql_errno(&my_connection)) {

   fprintf(stderr, "Connection error %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection))

  }

 }

 return EXIT_SUCCESS;

}

void display_header() {

 MYSQL_FIELD *field_ptr;

 printf("Column details:\n");

 while ((field_ptr = mysql_fetch_field(res_ptr)) != NULL) {

  printf("\t Name: %s\n", field_ptr->name);

  printf("\t Type: ");

  if (IS_NUM(field_ptr->type)) {

   printf("Numeric field\n");

  } else {

   switch(field_ptr->type) {

   case FIELD_TYPE_VAR_STRING:

    printf("VARCHAR\n");

    break;

   case FIELD_TYPE_LONG:

    printf("LONG\n");

    break;

   default:

    printf("Type is %d, check in mysql_com.h\n", field_ptr->type);

   } /* switch */

  } /* else */

  printf("\t Max width %ld\n", field_ptr->length);

  if (field_ptr->flags & AUTO_INCREMENT_FLAG)

   printf("\t Auto increments\n");

  printf("\n");

 } /* while */

}

void display_row() {

 unsigned int field_count;

 field_count = 0;

 while (field_count < mysql_field_count(&my_connection)) {

  if (sqlrow[field_count]) printf("%s ", sqlrow[field_count]);

  else printf("NULL");

  field_count++;

 }

 printf("\n");

}

Когда вы откомпилируете и выполните программу, то получите следующий вывод:

$ ./select4

Connection success

Column details:

      Name: childno

      Type: Numeric field

      Max width 11

      Auto increments

      Name: fname

      Type: VARCHAR

      Max width 30

      Name: age

      Type: Numeric field

      Max width 11

Column details:

1 Jenny 21

2 Andrew 17

$

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

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

Разные функции

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

Таблица 8.13

Пример API-вызова Описание
char *mysql_get_client_info(void); Возвращает данные о версии библиотеки, используемой клиентской программой
char *mysql_get_host_info(MYSQL *connection); Возвращает информацию о подключении к серверу
char *mysql_get_server_info(MYSQL *connection); Возвращает информацию о сервере, к которому вы в данный момент подключены
char *mysql_info(MYSQL* connection); Возвращает информацию о самом последнем выполненном запросе, но работает только с запросами нескольких типов — обычно с операторами INSERT и UPDATE. В противном случае возвращает NULL
int mysql_select_db(MYSQL *connection, const char *dbname); Заменяет базу данных, применяемую по умолчанию, на заданную в качестве параметра, при условии, что у пользователя есть соответствующие права доступа. В случае успеха возвращает ноль
int mysql_shutdown(MYSQL* connection, enum mysql_enum_shutdown level); Если у вас есть соответствующие права, завершает работу сервера базы данных, к которому вы подключены. В этот момент уровень останова следует задать равным SHUTDOWN_DEFAULT. В случае успеха возвращает ноль

Приложение для работы с базой данных компакт-дисков

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

Начните с создания новой базы данных и затем сделайте ее текущей базой данных.

mysql> create database blpcd;

Query OK, 1 row affected (0.00 sec)

mysql> use blpcd

Connection id: 10

Current database: blpcd

mysql>

Теперь вы готовы к проектированию и созданию необходимых вам таблиц.

Эта версия немного сложнее предыдущей, потому что вы выделите три отдельных элемента компакт-диска: исполнителя (или группу), элемент главного каталога и дорожки. Если вы подумаете о коллекции компакт-дисков и компонентах, ее составляющих, то поймете, что каждый компакт-диск состоит из ряда разных дорожек, но различные компакт-диски связаны друг с другом многими параметрами: исполнителем или группой, компанией, производящей их, представленным музыкальным стилем и т.д.

Вы могли бы сделать базу данных очень сложной, попытавшись сохранить все эти разные параметры, но в данном примере ограничьте себя только двумя самыми важными связями.

Во-первых, каждый компакт-диск состоит из переменного количества дорожек, поэтому вы будете хранить данные о дорожке в таблице, отделенной от остальных данных компакт-диска. Во-вторых, у каждого исполнителя (или группы) часто несколько альбомов, поэтому было бы удобно сохранять сведения об исполнителе один раз, а затем отдельно извлекать все компакт-диски, записанные этим исполнителем. Вы не будете пытаться разбить группы на разных исполнителей, которые, возможно, выпустили сольные альбомы, или работать со сборными компакт-дисками — вы будете стараться сохранять структуру вашей коллекции простой!

Связи тоже сохраняйте очень простыми — каждый исполнитель (им может быть название группы) выпустил один или несколько компакт-дисков и каждый компакт-диск состоит из одной или нескольких дорожек. Связи или отношения в вашей базе данных представлены на рис. 8.8.

Рис. 8.8

Создание таблиц

Сейчас вы должны определить реальную структуру таблиц. Начните с основной таблицы — таблицы компакт-дисков (cd), в которой хранится большая часть информации. Вам нужно сохранять идентификационный номер (id) компакт-диска, номер каталога, название и, возможно, ваши собственные заметки. Вам также понадобится ID-номер из таблицы исполнителей, чтобы знать, какой исполнитель выпустил альбом.

Таблица исполнителей (artist) очень проста; сохраните в ней только имя исполнителя и уникальный идентификационный номер (id) исполнителя. Таблица дорожек (track) также чрезвычайно проста; вам нужен только ID компакт-диска, чтобы знать, к какому CD относится дорожка, номер дорожки и название дорожки.

Сначала таблица компакт-диска:

CREATE TABLE cd (

 id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY,

 title VARCHAR(70) NOT NULL,

 artist_id INTEGER NOT NULL,

 catalogue VARCHAR(30) NOT NULL,

 notes VARCHAR(100)

);

Приведенный программный код создает таблицу с именем cd со следующими столбцами:

□ столбец id, содержащий целое число, которое автоматически увеличивается и представляет собой первичный ключ таблицы;

□ столбец title длиной до 70 символов;

□ столбец artist_id — целое число, которое будет использоваться в таблице artist;

□ столбец catalogue — номер длиной до 30 символов;

□ столбец notes до 100 символов.

Учтите, что только столбец notes может быть NULL; у всех остальных должны быть значения.

Теперь таблица artist:

CREATE TABLE artist (

 id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY,

 name VARCHAR(100) NOT NULL

);

И снова у вас столбец id и еще один для имени исполнителя.

И наконец, таблица track:

CREATE TABLE track (

 cd_id INTEGER NOT NULL,

 track_id INTEGER NOT NULL,

 title VARCHAR(70),

 PRIMARY KEY(cd_id, track_id)

);

Обратите внимание на то, что на этот раз вы объявили первичный ключ несколько иначе. Таблица track необычна тем, что ID каждого компакт-диска будет появляться несколько раз, ID любой заданной дорожки, скажем, дорожки 1, также будет появляться несколько раз в разных компакт-дисках. Но их комбинация всегда будет уникальной, поэтому объявите ваш первичный ключ как комбинацию двух столбцов. Такой ключ называют составным, поскольку он состоит из нескольких столбцов, участвующих в комбинации.

Сохраните эти SQL-операторы в текущем каталоге, в файле, названном create_tables.sql, и затем двигайтесь дальше и создайте базу данных и таблицы в ней. Готовый пример сценария содержит дополнительные строки, по умолчанию помеченные как комментарий, в них удаляются эти таблицы, если они уже существуют.

$ mysql -u rick -р

Enter password:

Welcome to the MySQL monitor. Commands end with ; or \g.

mysql> use blpcd;

Database changed

mysql> \. create_tables.sql

Query OK, 0 rows affected (6.04 sec)

Query OK, 0 rows affected (0.10 sec)

Query OK, 0 rows affected (0.00. sec)

mysql>

Обратите внимание на применение команды \. для получения ввода из файла create_tables.sql.

Вы могли бы создать таблицы, выполнив операторы SQL или просто набирая данные с помощью обозревателя запросов MySQL Query Browser.

После того как таблицы созданы, их можно просмотреть, используя MySQL Administrator (рис. 8.9), в котором вы проверяете таблицу индексов базы данных blpcd (или схему, если вы предпочитаете этот термин).

Выбрав редактирование таблицы (щелчок правой кнопкой мыши или двойной щелчок мышью имени таблицы на вкладке Tables (Таблицы)) позволит увидеть дополнительные сведения о столбцах (рис. 8.10).

Рис. 8.9

Рис. 8.10 

Вы заметили два значка ключа рядом со столбцами cd_id и track_id на рис. 8.10? Это означает, что они оба участвуют в формировании составного первичного ключа. Разрешив названию дорожки быть равным NULL (условие NOT NULL не проверяется), вы допускаете наличие нетипичной, но иногда встречающейся дорожки компакт-диска, не имеющей названия.

Вставка данных

Теперь вам нужно вставить какие-нибудь данные. Лучший способ проверки любого проекта базы данных — вставка контрольных данных и проверка работоспособности проекта.

Далее мы продемонстрируем пример импорта тестовых данных, что не важно для понимания происходящего, т. к. все операции импорта в основном похожи — они загружают разные таблицы. Есть два важных аспекта, на которые здесь следует обратить внимание.

□ Сценарий удаляет любые имеющиеся данные, чтобы начать с "чистого листа".

□ В поля id вставляются значения вместо использования функции AUTO_INCREMENT. В данном случае это безопаснее, поскольку при вставках необходимо знать, какие значения применялись, чтобы убедиться в полной корректности отношений между разными данными, поэтому лучше ввести значения, чем разрешить средству AUTO_INCREMENT автоматически сгенерировать их.

Этот файл назван insert_data.sql и может быть выполнен с помощью команды \., которую вы уже видели:

-- Удаляются существующие данные

delete from track;

delete from cd;

delete from artist;

-- Теперь данные вставляются

-- Сначала таблица artist (исполнители или группы)

insert into artist(id, name) values(1, 'Pink Floyd');

insert into artist(id, name) values(2, 'Genesis');

insert into artist(id, name) values(3, 'Einaudi');

insert into artist(id, name) values(4, 'Melanie C');

-- Затем таблица cd

insert into cd(id, title, artist_id, catalogue) values(1, 'Dark Side of the Moon', 1, 'B000024D4P');

insert into cd(id, title, artist_id, catalogue) values(2, 'Wish You Were Here', 1, 'B000024D4S');

insert into cd(id, title, artist_id, catalogue) values(3, 'A Trick of the Tail', 2, 'B000024EXM');

insert into cd(id, title, artist_id, catalogue) values(4, 'Selling England By the Pound', 2, 'B000024E9M');

insert into cd(id, title, artist_id, catalogue) values(5, 'I Giorni', 3, 'B000071WEV');

insert into cd(id, title, artist_id, catalogue) values(6, 'Northern Star', 4, 'B00004YMST');

--- Заполнение дорожек

insert into track(cd_id, track_id, title) values(1, 1, 'Speak to me');

insert into track(cd_id, track_id, title) values(1, 2, 'Breathe');

и оставшиеся дорожки этого альбома и следующий альбом:

insert into track(cd_id, track_id, title) values(2, 1, 'Shine on you crazy diamond');

insert into track(cd_id, track_id, title) values(2, 2, 'Welcome to the machine');

insert into track(cd_id, track_id, title) values(2, 3, 'Have a cigar');

insert into track(cd_id, track_id, title) values(2, 4, 'Wish you were here');

insert into track(cd_id, track_id, title) values(2, 5, 'Shine on you crazy diamond pt.2');

и т.д.

insert into track(cd_id, track_id, title) values(5, 1, 'Melodia Africana (part 1)';

insert into track(cd_id, track_id, title) values(5, 2, 'I due fiumi');

insert into track(cd_id, track_id, title) values(5, 3, 'In un\'altra vita');

…до финальных дорожек:

insert into track(cd_id, track_id, title) values(6, 11, 'Closer');

insert into track(cd_id, track_id, title) values(6, 12, 'Feel The Sun');

Далее сохраните это в файле pop_tables.sql и выполните его, как и раньше, из командной строки монитора mysql с помощью команды \..

Примечание

Обратите внимание на то, что в cd_id=5 ("I Giorni") с track=3 название In un'altra vita содержит апостроф. Для вставки его в базу данных вы должны использовать обратный слэш (\).

Теперь самое время убедиться в том, что ваши данные выглядят осмысленно. Для этого можно применить программу-клиент mysql в режиме командной строки и SQL-операторы. Начните с выбора двух первых дорожек из каждого альбома в вашей базе данных.

SELECT artist.name, cd.title AS "CD Title", track.track_id, track.title AS "Track" FROM artist, cd, track WHERE artist.id = cd.artist_id AND track.cd_id = cd.id AND track.track_id < 3

Если вы выполните этот оператор в MySQL Query Browser, то увидите, что данные выглядят нормально (рис. 8.11).

SQL-оператор на первый взгляд сложноват, но это можно исправить, рассматривая его последовательно по частям.

Если игнорировать части AS в операторе SELECT, его первая часть такова:

SELECT artist.name, cd.title, track.track_id, track.title

Она просто сообщает о том, какие столбцы вы хотите отобразить, используя форму записи имя_таблицы.имя_столбца.Рис. 8.11 

Части AS оператора SELECT

SELECT artist.name, cd.title AS "CD Title", track.track_id, and track.title AS "Track"

просто переименовывают столбцы в отображаемом выводе. Таким образом, заголовок столбца title из таблицы cd (cd.title) называется "CD Title", а столбец track.track.id — "Track". Подобное использование ключевого слова AS обеспечивает более дружественный по отношению к пользователю вывод. Вы практически никогда не будете применять эти имена при вызове SQL-операторов из другого языка программирования, но ключевое слово as полезно при работе с SQL-операторами из командной строки.

Следующая часть тоже понятна: она сообщает серверу имена таблиц, которые вы используете:

FROM artist, cd, track

Часть WHERE слегка мудреная:

WHERE artist.id = cd.artist_id AND track.cd_id = cd.id AND track.track_id < 3

Первый фрагмент сообщает серверу о том, что id в таблице artist такой же, как номер в столбце artist_id таблицы cd. Напоминаем, что вы сохраняете имя исполнителя один раз и используете id для ссылки на этого исполнителя в таблице cd. Следующий фрагмент, track.cd_id = cd.id, проделывает то же самое для таблиц track и cd, извещая сервер о том, что столбец cd_id таблицы track такой же, как столбец id таблицы cd. Третий фрагмент, track.track_id < 3, ограничивает объем возвращаемых данных так, что вы получаете только дорожки 1 и 2 из каждого компакт-диска. Последнее, но не по значимости, объединение этих трех условий с помощью операции AND, т.к. вы хотите, чтобы все три условия были истинными.

Доступ к данным приложения из программы на С

В этой главе вы не готовы писать законченное приложение, применяющее интерфейс GUI. Прежде надо сконцентрироваться на написании файла интерфейса, позволяющего сравнительно просто получить доступ, к вашим данным из программы на языке С. Общая проблема при написании подобного программного кода — неизвестные объем данных, которые могут быть возвращены, и способ передачи их между программой-клиентом и программой, обращающейся к базе данных. В данном приложении, для того чтобы сохранить его простоту и сосредоточиться на интерфейсе базы данных, очень важной части программного кода, будут применяться структуры фиксированного размера. В реальном приложении этот вариант может оказаться неприемлемым. Универсальное решение, также облегчающее сетевой трафик, — всегда извлекать данные построчно с помощью функций mysql_use_result и mysql_fetch_row, как было показано ранее в этой главе.

Определение интерфейса

Начните с заголовочного файла app_mysql.h, в котором определяются структуры и функции.

Сначала несколько структур:

/* Упрощенная структура для представления компакт-диска

   за исключением информации о дорожке */

struct current_cd_st {

 int artist_id;

 int cd_id;

 char artist_name[100];

 char title[100];

 char catalogue[100];

};

/* Упрощенная структура сведений о дорожке */

struct current_tracks_st {

 int cd_id;

 char track[20][100];

};

#define MAX_CD_RESULT 10

struct cd_search_st {

 int cd_id[MAX_CD_RESULT];

};

Далее пара функций для подключения к серверу и отключения от него:

/* Серверные функции базы данных */

int database_start(char *name, char *password);

void database_end();

Теперь перейдем к функциям манипулирования данными. Обратите внимание на отсутствие функций создания и удаления исполнителей. Вы реализуете их за кадром, создавая необходимые записи об исполнителях и затем удаляя их, когда их упоминания не остается ни в одном альбоме.

/* Функции для добавления компакт-диска */

int add_cd(char *artist, char *title, char *catalogue, int *cd_id);

int add_tracks(struct current_tracks_st *tracks);

/* Функции поиска и извлечения компакт-диска */

int find_cds(char *search_str, struct cd_search_st *results);

int get_cd(int cd_id, struct current_cd_st *dest);

int get_cd_tracks(int cd_id, struct current_tracks_st *dest);

/* Функция для удаления элементов */

int delete_cd(int cd_id);

Функция поиска очень обобщенная: вы передаете строку, и она ищет эту строку в элементах "исполнитель", "название" или "каталог".

Тестирование интерфейса приложения

Перед реализацией вашего интерфейса следует написать программный код, использующий его. Это может показаться странным, но зачастую это хороший способ подробно проанализировать поведение интерфейса, прежде чем принимать решение о его реализации.

Далее приведена программа app_test.c. Сначала несколько файлов include и типов structs:

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include "app_mysql.h"

int main() {

 struct current_cd_st cd;

 struct cd_search_st cd_res;

 struct current_tracks_st ct;

 int cd_id;

 int res, i;

Первое, что всегда должно делать ваше приложение, — инициализация подключения к базе данных, предоставляющая корректные имя пользователя и пароль (убедитесь, что вы заменили их своими):

 database_start("rick", "secret");

Далее тестируется добавление компакт-диска:

 res = add_cd("Mahler", "Symphony No 1", "4596102", &cd_id);

 printf("Result of adding a cd was %d, cd_id is %d\n", res, cd_id);

 memset(&ct, 0, sizeof(ct));

 ct.cd_id = cd_id;

 strcpy(ct.track[0], "Langsam Schleppend");

 strcpy(ct.track[1], "Kraftig bewegt");

 strcpy(ct.track[2], "Feierlich und gemessen");

 strcpy(ct.track[3], "Stürmisch bewegt");

 add_tracks(set);

Теперь поищите компакт-диск и извлеките информацию из первого найденного CD:

 res = find_cds("Symphony", &cd_res);

 printf("Found %d cds, first has ID %d\n", res, cd_res.cd_id[0]);

 res = get_cd(cd_res.cd_id[0], &cd);

 printf("get_cd returned %d\n", res);

 memset(&ct, 0, sizeof(ct));

 res = get_cd_tracks(cd_res.cd_id[0], set);

 printf("get_cd_tracks returned %d\n", res);

 printf("Title: %s\n", cd.title);

 i = 0;

 while (i < res) {

  printf("\ttrack %d is %s\n", i, ct.track[i]);

  i++;

 }

В заключение удалите компакт-диск:

 res = delete_cd(cd_res.cd_id[0]);

 printf("Delete_cd returned %d\n", res);

Затем отключитесь и завершите работу программы:

 database_end();

 return EXIT_SUCCESS;

}

Реализация интерфейса

Теперь более трудная часть — реализация интерфейса, описанного вами. Вся она хранится в файле app_mysql.с.

Начните с основных файлов include, глобальной структуры подключения, которая понадобится, и флага dbconnected, который будет применяться для того, чтобы приложения не пытались получить доступ к данным, если у них нет подключения. Вы также используете внутреннюю функцию get_artist_id, для улучшения структуры программы:

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include "mysql.h"

#include "app_mysql.h"

static MYSQL my_connection;

static int dbconnected = 0;

static int get_artist_id(char *artist);

Как вы видели ранее в этой главе, подключиться к базе данных очень просто, а отключиться от нее и того проще:

int database_start(char *name, char *pwd) {

 if (dbconnected) return 1;

 mysql_init(&my_connection);

 if (!mysql_real_connect(&my_connection, "localhost",

  name, pwd, "blpcd", 0, NULL, 0)) {

  fprintf(stderr, "Database connection failure: %d, %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

  return 0;

 }

 dbconnected = 1;

 return 1;

} /* database_start */

void database_end() {

 if (dbconnected) mysql_close(&my_connection);

 dbconnected = 0;

} /* database_end */

Начинается реальная работа благодаря функции add_cd. Вам нужны сначала несколько объявлений и санитарная проверка, Чтобы убедиться в наличии подключения к базе данных. Вы увидите ее во всех написанных функциях, доступных извне.

Напоминаем о том, что программа будет отслеживать имена исполнителей автоматически:

int add_cd(char *artist, char *title, char* catalogue, int *cd_id) {

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 int res;

 char is[250];

 char es[250];

 int artist_id = -1;

 int new_cd_id = -1;

 if (!dbconnected) return 0;

Далее нужно проверить, существует ли уже исполнитель, если нет, то создать его. Обо всем этом заботится функция get_artist_id, которую вы скоро увидите:

 artist_id = get_artist_id(artist);

Теперь, имея artist_id, вы можете вставлять главную запись компакт-диска. Обратите внимание на применение функции mysql_escape_string, не допускающей специальных символов в названии компакт-диска.

 mysql_escape_string(es, title, strlen(title));

 sprintf(is,

  "INSERT INTO cd(title, artist_id, catalogue) VALUES('%s', %d, '%s')",

  es, artist_id, catalogue);

 res = mysql_query(&my_connection, is);

 if (res) {

  fprintf(stderr, "Insert error %d: %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

  return 0;

 }

Когда вы дойдете до вставки дорожек для данного компакт-диска, вам потребуется знать ID, который использовался при вставке записи о компакт-диске. Вы сделали поле автоматически наращиваемым, поэтому база данных автоматически присвоила ID, но вам нужно явно извлечь это значение. Как было показано ранее в этой главе, сделать это можно с помощью функции LAST_INSERT_ID.

 res = mysql_query(&my_connection, "SELECT LAST_INSERT_ID()");

 if (res) {

  printf("SELECT error: %s\n", mysql_error(&my_connection));

  return 0;

 } else {

  res_ptr = mysql_use_result(&my_connection);

  if (res_ptr) {

   if ((mysqlrow = mysql_fetch_row(res_ptr))) {

    sscanf(mysqlrow[0], "%d", &new_cd_id);

   }

   mysql_free_result(res_ptr);

  }

He стоит беспокоиться о других программах-клиентах, вставляющих компакт-диски в это же время, и о возможной путанице поступающих номеров ID; СУРБД MySQL запоминает присвоенный ID для каждого подключения, поэтому, даже если другое приложение вставило компакт-диск прежде, чем вы извлекли ID, вы все равно получите номер, соответствующий вашей строке, а не строке, добавленной другим приложением.

И последнее, но не по степени важности, установите ID вновь добавленной строки и верните код успешного или аварийного завершения:

  *cd_id = new_cd_id;

  if (new_cd_id != -1) return 1;

  return 0;

 }

} /* add_cd */

Теперь посмотрите реализацию функции get_artist_id; процесс очень похож на вставку записи о компакт-диске:

/* Поиск или создание artist_id для заданной строки */

static int get_artist_id(char *artist) {

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 int res;

 char qs[250];

 char is[250];

 char es[250];

 int artist_id = -1;

 /* Он уже существует? */

 mysql_escape string(es, artist, strlen(artist));

 sprintf(qs, "SELECT id FROM artist WHERE name = '%s'", es);

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

 } else {

  res_ptr = mysql_store_result(&my_connection);

  if (res_ptr) {

   if (mysqr_num_rows(res_ptr) > 0) {

    if (mysqlrow = mysql_fetch_row(res_ptr)) {

     sscanf(mysqlrow[0], "%d", &artist_id);

    }

   }

   mysql_free_result(res_ptr);

  }

 }

 if (artist_id != -1) return artist_id;

 sprintf(is, "INSERT INTO artist(name) VALUES ('%s')", es);

 res = mysql_query(&my_connection, is);

 if (res) {

  fprintf(stderr, "Insert error %d: %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

  return 0;

 }

 res = mysql_query(&my_connection, "SELECT LAST_INSERT_ID()");

 if (res) {

  printf("SELECT error: %s\n", mysql_error(&my_connection));

  return 0;

 } else {

  res_ptr = mysql_use_result(&my_connection);

  if (res_ptr) {

   if ((mysqlrow = mysql_fetch_row(res_ptr))) {

    sscanf(mysqlrow[0], "%d", &artist_id);

   }

   mysql_free_result(res_ptr);

  }

 }

 return artist_id;

} /* get_artist_id */

Переходите к вставке информации о дорожках для вашего компакт-диска. И снова защититесь от специальных символов в названиях дорожек:

int add_tracks(struct current_tracks_st *tracks) {

 int res;

 char is[250];

 char es[250];

 int i;

 if (!dbconnected) return 0;

 i = 0;

 while (tracks->track[i][0]) {

  mysql_escape_string(es, tracks->track[i], strlen(tracks->track[i]));

  sprintf(is,

   "INSERT INTO track(cd_id, track_id, title) VALUES(%d, %d, '%s')",

  tracks->cd_id, i + 1, es);

  res = mysql_query(&my_connection, is);

  if (res) {

   fprintf(stderr, "Insert error %d: %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

   return 0;

  }

  i++;

 }

 return 1;

} /* add tracks */

Теперь переходите к извлечению информации о компакт-диске с заданным значением его ID. Будет применена операция объединения базы данных для извлечения ID исполнителя во время получения данных об ID диска. Это обычно хороший подход: системы управления базами данных отлично знают, как эффективно выполнять сложные запросы, поэтому никогда не пишите прикладной программный код для того, что вы можете просто попросить сделать СУРБД, передав ей запрос на языке SQL. Есть шанс сберечь собственные силы, не тратя их на написание дополнительного программного кода, и получить приложение, работающее более эффективно, разрешив СУРБД выполнить максимально возможный объем работы.

int get_cd(int cd_id, struct current_cd_st *dest) {

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 int res;

 char qs[250];

 if (!dbconnected) return 0;

 memset(dest, 0, sizeof(*dest));

 dest->artist_id = -1;

 sprintf(qs, "SELECT artist.id, cd.id, artist.name, cd.title, cd.catalogue \

  FROM artist, cd WHERE artist.id = cd.artist_id and cd.id = %d", cd_id);

 res = mysql_query(&my_cormection, qs);

 if (res) {

  fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

 } else {

  res_ptr = mysql_store_result(&my_connection);

  if (res_ptr) {

   if (mysql_num_rows(res_ptr) > 0) {

    if (mysqlrow = mysql_fetch_row(res_ptr)) {

     sscanf(mysqlrow[0], "%d", &dest->artist_id);

     sscanf(mysqlrow[1], "%d", &dest->cd_id);

     strcpy(dest->artist_name, mysqlrow[2]);

     strcpy(dest->title, mysqlrow[3]);

     strcpy(dest->catalogue, mysqlrow[4]);

    }

   }

   mysql_free_result(res_ptr);

  }

 }

 if (dest->artist_id != -1) return 1;

 return 0;

} /* get_cd */

Далее вы реализуете извлечение информации о дорожках. В SQL-операторе вы задаете ключевые слова ORDER BY, для того чтобы возвращать дорожки в подходящей последовательности. И опять это позволит СУРБД выполнить нужную работу более эффективно, чем если бы вы извлекли дорожки в произвольном порядке, а затем написали собственный программный код для их сортировки.

int get_cd_tracks(int cd_id, struct current_tracks_st *dest) {

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 int res;

 char qs[250];

 int i = 0, num_tracks = 0;

 if (!dbconnected) return 0;

 memset(dest, 0, sizeof(*dest));

 dest->cd_id = -1;

 sprintf(qs, "SELECT track_id, title FROM track WHERE track.cd_id = %d \

  ORDER BY track_id", cd_id);

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

 } else {

  res_ptr = mysql_store_result(&my_connection);

  if (res_ptr) {

   if ((num_tracks = mysql_num_rows(res_ptr)) > 0) {

    while (mysqlrow = mysql_fetch_row(res_ptr)) {

     strcpy(dest->track[i], mysqlrow[1]);

     i++;

    }

    dest->cd_id = cd_id;

   }

   mysql_free_result(res_ptr);

  }

 }

 return num_tracks;

} /* get_cd_tracks */

До сих пор вы добавляли и извлекали информацию о компакт-дисках. Вы добились простоты интерфейса, ограничив число результатов, которые могут быть возвращены, но вам все же нужна собственная функция, сообщающая о том, сколько строк в результирующем наборе, даже если их больше, чем вы можете извлечь.

int find_cds(char *search_str, struct cd_search_st *dest) {

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 int res;

 char qs[500];

 int i = 0;

 char ss[250];

 int num_rows = 0;

 if (!dbconnected) return 0;

Очистите структуру, хранящую результат, и защитите ее от специальных символов в строке запроса:

 memset(dest, -1, sizeof(*dest));

 mysql_escape_string(ss, search_str, strlen(search_str));

Далее вы формируете строку запроса. Обратите внимание на необходимость применения большого количества символов %, т.к. знак % — это и символ, который необходимо включить в SQL-оператор для указания соответствия любой строке и специальный символ в функции sprintf:

 sprintf(qs, "SELECT DISTINCT artist.id, cd.id FROM artist, cd WHERE artist.id = cd.artist_id and (artist.name LIKE '%%%s%%' OR cd.title LIKE '%%%s%%' OR cd.catalogue LIKE '%%%s%%')", ss, ss, ss);

Сейчас можно выполнить запрос:

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

 } else {

  res_ptr = mysql_store_result(&my_connection);

  if (res_ptr) {

   num_rows = mysql_num_rows(res_ptr);

   if (num_rows > 0) {

    while ((mysqlrow = mysql_fetch_row(res_ptr)) && i < MAX_CD_RESULT) {

     sscanf(mysqlrow[1], "%d", &dest->cd_id[i]);

     i++;

    }

   }

   mysql_free_result(res_ptr);

  }

 }

 return num_rows;

} /* find_cds */

Последнее, но не по значимости, — ваша реализация способа удаления компакт-дисков. В соответствии с политикой скрытого управления элементами таблицы исполнителей вы будете удалять исполнителя заданного компакт-диска, если нет других дисков с той же самой строкой исполнителя. Удивительно, но в языке SQL нет средств описания удаления из нескольких таблиц, поэтому вы должны удалять данные из каждой таблицы по очереди:

int delete_cd(int cd_id) {

 int res;

 char qs[250];

 int artist_id, num_rows;

 MYSQL_RES *res_ptr;

 MYSQL_ROW mysqlrow;

 if (!dbconnected) return 0;

 artist_id = -1;

 sprintf(qs, "SELECT artist_id FROM cd WHERE artist_id = \

(SELECT artist_id FROM cd WHERE id = '%d')", cd_id);

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "SELECT error: %s\n", mysql_error(&my_connection));

 } else {

  res_ptr = mysql_store_result(&my_connection);

  if (res_ptr) {

   num_rows = mysql_num_rows(res_ptr);

   if (num_rows == 1) {

    /* Исполнитель не упоминается в других CD */

    mysqlrow = mysql_fetch_row(res_ptr);

    sscanf(mysqlrow[0], "%d", &artist_id);

   }

   mysql_free_result(res_ptr);

  }

 }

 sprintf(qs, "DELETE FROM track WHERE cd_id = '%d'", cd_id);

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "Delete error (track) %d: %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

  return 0;

 }

 sprintf(qs, "DELETE FROM cd WHERE id = '%d'", cd_id);

 res = mysql_query(&my_connection, qs);

 if (res) {

  fprintf(stderr, "Delete error (cd) %d: %s\n",

   mysql_errno(&my_connection), mysql_error(&my_connection));

  return 0;

 }

 if (artist_id != -1) {

  /* Теперь элемент artist не связан ни с одним CD, удалите его */

  sprintf(qs, "DELETE FROM artist WHERE id = '%d'", artist_id);

  res = mysqlquery(&my_connection, qs);

  if (res) {

   fprintf(stderr, "Delete error (artist) %d: %s\n",

    mysql_errno(&my_connection), mysql_error(&my_connection));

  }

 }

 return 1;

} /* delete_cd */

На этом программный код завершается.

Для законченности и облегчения жизни добавьте файл Makefile. Возможно, вам придется откорректировать в нем путь к файлам include, зависящий от установки СУРБД MySQL в вашей системе.

all: арр

арр: app_mysql.с app_test.с app_mysql.h

 gcc -о app -I/usr/include/mysql appmysql.с app_test.с -lmysqlclient -L/usr/lib/mysql

В последующих главах вы увидите применение этого интерфейса с реальным интерфейсом GUI. Сейчас, если вы хотите увидеть изменения в базе данных по мере выполнения программы, мы предлагаем в одном окне выполнить программу по шагам с помощью отладчика gdb, а в другом следить за изменениями в базе данных. Если вы будете использовать MySQL Query Browser, не забудьте о необходимости обновлять отображаемые данные для отслеживания текущих изменений.

Резюме 

В этой главе мы кратко рассмотрели СУРБД MySQL. Более опытные пользователи обнаружат, что многие сложные средства не обсуждались в данной главе, например, ограничения внешнего ключа и триггеры.

Вы получили основные сведения об установке MySQL и узнали об основах администрирования баз данных MySQL с помощью утилит-клиентов. Мы рассмотрели API языка С, который наряду с другими языками программирования может применяться с СУРБД MySQL. Вы также познакомились с некоторыми операторами языка SQL в действии.

Мы надеемся, что эта глава вдохновит вас на использование баз данных на основе SQL для хранения ваших данных и заставит искать дополнительную информацию об этих мощных средствах управления базами данных.

И как напоминание, основной информационный ресурс MySQL — исходная страница MySQL на сайте www.mysql.com. 

Глава 9

Средства разработки

В этой главе рассматриваются средства разработки программ, применяемые в ОС Linux, некоторые из них доступны и в ОС UNIX. В дополнение к обязательным составляющим, таким как компиляторы и отладчики, Linux предлагает ряд средств, каждое из которых предназначено для одного вида работы и позволяет разработчику комбинировать эти средства новыми оригинальными способами. Такой подход — часть идеологии UNIX, которую унаследовала ОС Linux. В данной главе рассматривается несколько наиболее важных средств разработки и показан ряд примеров их использования для решения проблем. К этим средствам относятся следующие:

□ команда make и make-файлы;

□ управление исходным программным кодом с помощью RCS и CVS;

□ написание интерактивного руководства;

□ распространение программного обеспечения с помощью patch и tar;

□ среды разработки.

Проблемы применения многочисленных исходных файлов

При написании маленьких программ многие программисты после корректировки просто перестраивают свое приложение, компилируя заново все файлы. Но в случае больших программ такой простой подход порождает проблемы. Время цикла "редактирование — компиляция — тестирование" быстро растет. Даже самому терпеливому программисту хотелось бы избежать перекомпиляции всех файлов при изменении лишь одного из них. 

Гораздо более серьезная проблема может возникнуть при создании многочисленных заголовочных файлов и включении их в разные исходные файлы. Предположим, что у вас есть заголовочные файлы a.h, b.h и c.h и исходные файлы на языке С main.c, 2.с и 3.c (мы надеемся, что в реальных проектах вы выберете более удачные имена, чем приведенные здесь). Вы можете столкнуться со следующей ситуацией.

/* main.c */

#include "a.h"

...

/* 2.с */

#include "a.h"

#include "b.h"

...

/* 3.c */

#include "b.h"

#include "c.h"

...

Если программист изменяет файл c.h, файлы main.c и 2.с не нужно перекомпилировать, поскольку они не зависят от этого заголовочного файла. Файл 3.с зависит от c.h и, следовательно, должен быть откомпилирован заново, если изменился c.h. Но если был изменен файл b.h, и программист забыл откомпилировать заново файл 2.с, результирующая программа может перестать работать корректно.

Утилита make может решить обе эти проблемы, обеспечивая в случае необходимости перекомпиляцию всех файлов, затронутых изменениями.

Примечание

Команда make применяется не только для компиляции программ. Ее можно использовать, когда формируются выходные файлы из нескольких входных файлов. Ещё одно ее применение включает обработку документов (такую же, как с помощью программ troff или ТеХ).

Команда make и make-файлы

Несмотря на то, что у команды make много внутренних сведений и знаний, она не может самостоятельно решить, как скомпоновать ваше приложение. Вы должны предоставить файл, который сообщит ей, как устроено приложение. Этот файл называется make-файлом или файлом сборки.

Make-файл чаще всего расположен в том же каталоге, что и другие исходные файлы проекта. В любой момент времени на вашем компьютере может быть множество make-файлов. Действительно, если у вас большой проект, вы можете управлять им, используя отдельные make-файлы для разных частей проекта.

Сочетание команды make и make-файла — мощное средство управления проектами. Оно часто применяется не только для управления компиляцией исходного программного кода, но и для подготовки интерактивного справочного руководства или установки приложения в нужный каталог.

Синтаксис make-файлов

Make-файл состоит из набора зависимостей и правил. У зависимости есть цель или задание (выходной файл, который нужно создать) и набор исходных файлов, от которых она зависит. Правила или инструкции описывают, как создать выходной файл из зависимых файлов. Обычно цель — это единый исполняемый файл.

Make-файл читается командой make, определяющей выходной файл или файлы, которые должны быть сформированы, и затем сравнивающей даты и время исходных файлов, чтобы решить, какие инструкции нужно вызвать для формирования выходного файла. Часто следует выполнить другие промежуточные задания, прежде чем может быть сформирована заключительная цель. Команда make использует make-файл для определения порядка, в котором должны выполняться задания, и корректной последовательности запуска правил.

Опции и параметры make

У программы make есть несколько опций. Наиболее часто применяются следующие:

□ -k, сообщающая make о необходимости продолжать выполнение, если обнаружена ошибка, а не останавливаться при появлении первой проблемы. Эту опцию можно использовать, например, для выявления за один проход всех исходных файлов, которые не могут быть откомпилированы;

□ -n, сообщающая make о необходимости вывода перечня требуемых действий без реального их выполнения;

□ -f <файл>, позволяющая сообщить make о том, какой файл применять как make-файл. Если вы не используете эту опцию, стандартная версия программы make ищет в текущем каталоге первый файл, названный makefile. Если его нет, программа ищет файл, названный Makefile. Но если вы применяете GNU Make, что вероятно в ОС Linux, эта версия программы make сначала ищет файл GNUmakefile до поиска файла makefile и последующего поиска Makefile. В соответствии с соглашением программисты Linux применяют имя Makefile, которое позволяет поместить файл сборки первым в списке файлов каталога, заполненного именами файлов, состоящими из строчных букв. Мы полагаем, что вы не будете использовать имя GNUmakefile, поскольку оно специфично для. реализации GNU программы make.

Для того чтобы заставить команду make выполнить конкретное задание, как правило, собрать исполняемый файл, вы можете передать make имя задания или цели как параметр. Если этого не сделать, программа make попытается выполнить первое задание, указанное в make-файле. Многие программисты указывают в своих make-файлах в качестве первой цели или задания all и затем перечисляют остальные задания, как зависимости для all. Это соглашение делает понятным выбор задания по умолчанию, если не задана конкретная цель. Мы полагаем, что вы будете придерживаться этого соглашения.

Зависимости

Зависимости определяют, как каждый файл в окончательном приложении связан исходными файлами. В программном примере, приведенном ранее в этой главе, вы могли бы установить зависимости, говорящие о том, что вашему окончательному приложению требуются (оно зависит от) main.о, 2.о и 3.o; и также для main.о (main.c и a.h); 2.o (2.с, a.h и b.h) и 3.o (3.c, b.h и c.h). Таким образом, на файл main.о влияют изменения файлов main.c и a.h, и он нуждается в пересоздании с помощью повторной компиляции файла main.c, если был изменен любой из двух указанных файлов.

В make-файл вы записываете эти правила, указывая имя задания, двоеточие, пробелы или табуляции и затем разделенный пробелами или табуляциями перечень файлов, применяемых для создания выходного файла задания. Далее приведен список зависимостей для ранее приведенного примера:

myapp: main:о 2.о 3.o

main.о: main.c a.h

2.о: 2.с a.h b.h

3.o: 3.c b.h c.h

Список свидетельствует о том, что myapp зависит от main.о, 2.o и 3.o, a main.o зависит от main.c и a.h и т. д.

Данный набор зависимостей формирует иерархию, показывающую, как исходные файлы связаны друг с другом. Вы легко можете увидеть, что если изменяется b.h, то придется пересмотреть 2.o и 3.o, а поскольку 2.o и 3.o будут изменены, вам придется перестроить и myapp.

Если вы хотите собрать несколько файлов, можно использовать фиктивную цель или задание all. Предположим, что ваше приложение состоит из двоичного файла myapp и интерактивного руководства myapp.1. Описать их можно следующей строкой:

all: myapp myapp.1

И еще раз, если вы не включите задание all, программа make просто создаст выходной файл, первым найденный в make-файле.

Правила

Второй, компонент make-файла — правила или инструкции, описывающие способ создания выходного файла задания. В примере из предыдущего раздела какую команду следует применить после того, как команда make определила, что файл 2.o нуждается в перестройке? Возможно, достаточно простого применения команды gcc -с 2.с (и как вы увидите в дальнейшем, make на самом деле знает много стандартных правил), но что если вы хотите указать каталог include или задать опцию вывода символьной информации для последующей отладки? Сделать это можно, явно определив правила в make-файле.

Примечание

В данный момент мы должны информировать вас об очень странной и неудачной синтаксической записи, применяемой в make-файлах: разнице между пробелом и табуляцией. Все правила должны представлять собой строки, начинающиеся со знака табуляции; пробел не годится. Так как несколько пробелов и табуляция выглядят почти одинаково и поскольку почти во всех других случаях, касающихся программирования в системе Linux, нет большой разницы между пробелами и табуляциями, это может вызвать проблемы. Кроме того, пробел в конце строки в make-файле может вызвать сбой при выполнении команды make. Тем не менее, это исторический факт и в наше время слишком много make-файлов находится в обращении, чтобы можно было рассчитывать на изменение положения вещей, поэтому будьте внимательны! К счастью, если команда make не работает из-за пропущенной табуляции, это обычно довольно понятно.

А теперь выполните упражнение 9.1.

Упражнение 9.1. Простой make-файл

Большинство правил или инструкций состоит из простой команды, которая могла бы быть набрана в командной строке. Для примера создайте свой первый make-файл Makefile1:

myapp: main.о 2.o 3.o

 gcc -о myapp main.о 2.o 3.o

main.о: main.c a.h

 gcc -с main.c

2.о: 2.с a.h b.h

 gcc -с 2.с

3.o: 3.c b.h c.h

 gcc -с 3.c

Запустите команду make с опцией -f, потому что ваш make-файл не назван одним из стандартных имен makefile или Makefile. Если запустить приведенный код в каталоге, не содержащем исходных файлов, будет получено следующее сообщение:

$ make -f Makefile1

make: *** No rule to make target 'main.c', needed by 'main.o'. Stop.

$

Команда make предположила, что первое задание в make-файле, myapp, — это файл, который вы хотите создать. Затем она просмотрела остальные зависимости и прежде всего определила, что нужен файл, названный main.c. Поскольку вы все еще не создали этот файл и в make-файле не сказано, как он может быть создан, команда make вывела сообщение об ошибке. Итак, создайте исходные файлы и попробуйте снова. Поскольку результат нас не интересует, эти файлы могут быть очень простыми. Заголовочные файлы на самом деле пустые, поэтому вы можете создать их командой touch:

$ touch a.h

$ touch b.h

$ touch c.h

Файл main.c содержит функцию main, вызывающую функции function_two и function_three. В других двух файлах определены функции function_two и function_three. В исходных файлах есть строки #include для соответствующих заголовочных файлов, поэтому они оказываются зависимыми от содержимого включенных файлов заголовков. Это приложение не назовешь выдающимся, но, тем не менее, далее приведены листинги программ:

/* main.c */

#include <stdlib.h>

#include "a.h"

extern void function_two();

extern void function_three();

int main() {

 function_two();

 function_three();

 exit(EXIT_SUCCESS);

}

/* 2.c */

#include "a.h"

#include "b.h"

void function_two() { }

/* 3.с */

#include "b.h"

#include "c.h"

void function_three() { }

Теперь попробуйте выполнить команду make еще раз:

$ make -f Makefile1

gcc -с main.с gcc -с 2.с

gcc -с 3.с

gcc -о myapp main.о 2.о 3.о

$

На этот раз сборка прошла успешно.

Как это работает

Команда make обработала секцию зависимостей make-файла и определила файлы, которые нужно создать, и порядок их создания. Хотя вы сначала описали, как создать файл myapp, команда make определила правильный порядок создания файлов. Затем она запустила соответствующие команды для создания этих файлов, приведенные вами в секции правил. Команда make выводит на экран выполняемые ею команды. Теперь вы можете протестировать ваш make-файл, чтобы увидеть, корректно ли он обрабатывает изменения в файле b.h:

$ touch b.h

$ make -f Makefile1

gcc -c 2.с gcc -с 3.c

gcc -o myapp main.о 2.о 3.o

$

Команда make прочла ваш make-файл, определивший минимальное количество команд, требуемых для повторного построения myapp, и выполнила их в правильной последовательности. Теперь посмотрите, что произойдет, если вы удалите объектный файл:

$ rm 2.o

$ make -f Makefile1

gcc -с 2.c

gcc -о myapp main.о 2.о 3.о

$

И снова команда make правильно определяет нужные действия.

Комментарии в make-файле

Комментарий в make-файле начинается со знака # и продолжается до конца строки. Как и в исходных файлах на языке С, комментарии в make-файлах могут помочь как автору, так и другим пользователям понять, что имелось в виду во время написания данного файла.Sta

Макросы в make-файле

Даже если бы функциональные возможности команды make и make-файлов ограничивались тем, что уже описано, они были бы мощным средством управления проектами с множеством исходных файлов. Но эти средства становятся громоздкими и неповоротливыми в проектах, содержащих большое количество файлов. Поэтому make-файлы предоставляют возможность использовать макросы, позволяющие писать эти файлы в более обобщенном виде.

Макросы в make-файле записываются в виде конструкции MAСRONAME=значение, затем ссылаться на значение можно, указав $(MACRONAME) или ${MACRONAME}. Некоторые версии make могут также принимать $MACRONAME. Вы можете задать пустое значение макроса, оставив пустой часть строки после знака =.

Макросы часто используют в make-файлах для задания опций компилятора. Обычно во время разработки приложение компилируется без оптимизации и с включенной отладочной информацией. Для окончательной версии приложения, как правило, нужны другие режимы: маленький двоичный файл без какой-либо отладочной информации, работающий как можно быстрее.

Еще одна проблема в файле Makefile1 — жестко заданное имя компилятора gcc. В других UNIX-системах вы, возможно, будете использовать cc или c89. Если когда-нибудь вы захотите перенести ваш make-файл в другую версию UNIX или получите другой компилятор для имеющейся у вас системы, придется изменить несколько строк в вашем make-файле, чтобы заставить его работать. Макросы — хороший способ собрать все эти системнозависимые части и легко изменить их.

Обычно макросы определяются в самом make-файле, но их можно задать и при вызове команды make, если добавить определение макроса, например, make CC=c89. Определения, подобные данному, приведенные в командной строке, переопределяют заданные в make-файле определения. Заданные вне make-файла определения макросов должны передаваться как один аргумент, поэтому исключите пробелы или применяйте кавычки следующим образом: "CC = с89".

Выполните упражнение 9.2.

Упражнение 9.2. Make-файл с макросом

Далее приведена переработанная версия make-файла с именем Makefile2, в которой применяются макросы:

all: myapp

# Какой компилятор

СС = gcc

# Где хранятся файлы include

INCLUDE = .

# Опции для процесса разработки

СFLAGS = -g -Wall -ansi

# Опции для окончательной версии

# СFLAGS = -О -Wall -ansi

myapp: main.о 2.o 3.o

 $(CC) -о myapp main.о 2.o 3.o

main.о: main.c a.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -с main.c

2.о: 2.c a.h b.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c

3.o: 3.c b.h c.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c

Если удалить прошлую версию приложения и создать новую с помощью только что приведенного нового make-файла, вы получите следующее:

$ rm *.о myapp

$ make -f Makefile2

gcc -I. -g -Wall -ansi -c main.c

gcc -I. -g -Wall -ansi -c 2.c

gcc -I. -g -Wall -ansi -c 3.c

gcc -o myapp main.о 2.o 3.o

$

Как это работает

Программа make заменяет ссылки на макросы $(CC), $(CFLAGS) и $(INCLUDE) соответствующими определениями так же, как компилятор С поступает с директивами #define. Теперь, если вы захотите изменить команду компиляции, вам придется изменить только одну строку make-файла.

У команды make есть несколько специальных внутренних макросов, которые можно применять для того, чтобы еще более сократить make-файлы. В табл. 9.1 мы перечислили наиболее часто используемые из них; в последующих примерах вы увидите их в действии. Подстановка каждого из этих макросов выполняется только перед его использованием, поэтому значение макроса может меняться по мере обработки make-файла. На самом деле в этих макросах было бы очень мало пользы, если бы они действовали иначе.

Таблица 9.1

Макрос Определение
$? Список необходимых условий (файлов, от которых зависит выходной файл), измененных позже, чем текущий выходной файл
$@ Имя текущего задания
$< Имя текущего файла, от которого зависит выходной
$* Имя без суффикса текущего файла, от которого зависит выходной

Есть еще два полезных специальных символа, которые можно увидеть перед командами в make-файле:

□ символ - заставляет команду make игнорировать любые ошибки. Например, если вы хотели бы создать каталог и при этом игнорировать любые ошибки, скажем, потому что такой каталог уже существует, вы просто ставите знак "минус" перед командой mkdir. Чуть позже в этой главе вы увидите применение символа -;

□ символ @ запрещает команде make выводить команду в стандартный файл вывода перед ее выполнением. Этот символ очень удобен, если вы хотите использовать команду echo для вывода некоторых инструкций.

Множественные задания

Часто бывает полезно создать вместо одного выходного файла несколько или собрать несколько групп команд в одном файле. Вы можете сделать это, расширив свой make-файл. В упражнении 9.3 вы добавите задание clean на удаление ненужных объектных файлов, и задание install, перемещающее окончательное приложение в другой каталог.

Далее приведена следующая версия make-файла с именем Makefile3:

all: myapp

# Какой компилятор

CC = gcc

# Куда установить

# INSTDIR=/usr/local/bin

# Где хранятся файлы include

INCLUDE = .

# Опции для разработки

CFLAGS = -g -Wall -ansi

# Опции для рабочей версии

# CFLAGS = -О -Wall -ansi

myapp: main.o 2.o 3.o

 $(CC) -о myapp main.о 2.о 3.o

main.о: main.c a.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -c main.c

2.о: 2.c a.h b.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c

3.o: 3.c b.h c.h

 $(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c

clean:

 -rm main.o 2.o 3.o

install: myapp

 @if [ -d $(INSTDIR) ]; \

 then \

  cp myapp $(INSTDIR);\

  chmod a+x $(INSTDIR)/myapp;\

  chmod og-w $(INSTDIR)/myapp;\

  echo "Installed in $(INSTDIR)";\

 else \

  echo "Sorry, $(INSTDIR) does not exist";\

 fi

В этом make-файле есть несколько вещей, на которые следует обратить внимание. Во-первых, специальная цель all, которая задает только один выходной файл myapp. Следовательно, если вы выполняете make без указания задания, поведение по умолчанию — сборка файла myapp.

Следующая важная особенность относится к двум дополнительным заданиям: clean и install. В задании clean для удаления объектных файлов применяется команда rm. Команда начинается со знака -, тем самым сообщая команде make о необходимости игнорировать результат команды, поэтому make выполнится успешно, даже если объектных файлов нет и команда rm вернет ошибку. Правила для задания clean ни от чего не зависят, остаток строки после clean: пуст. Таким образом, задание всегда считается измененным со времени последнего выполнения, и его правило всегда выполняется, если clean указывается в качестве задания.

Задание install зависит от myapp, поэтому команда make знает, что должна создать myapp перед выполнением других команд задания install. Правила выполнения install состоят из нескольких команд сценария командной оболочки. Поскольку команда make запускает командную оболочку для выполнения правил и применяет новую командную оболочку для каждого правила, следует добавлять обратные слэши, чтобы все команды сценария были в одной логической строке и передавались для выполнения все вместе одному сеансу командной оболочки. Эта команда начинается со знака отменяющего вывод команды в стандартный файл вывода перед выполнением правила.

Задание install выполняет несколько команд одну за другой для установки приложения в указанное расположение. Оно не проверяет успешность выполнения предыдущей команды перед выполнением следующей. Если очень важно, чтобы последующие команды выполнялись только в случае успешного завершения предыдущей, можно написать команды, объединенные с помощью операции &&, как показано далее:

@if [ -d $(INSTDIR) ]; \

 then \

 cp myapp $(INSTDIR) &&\

 chmod a+x $(INSTDIR)/myapp && \

 chmod og-w $(INSTDIR/myapp && \

 echo "Installed in $(INSTDIR)" ; \

else \

 echo "Sorry, $(INSTDIR) does not exist"; false ; \

fi

Как вы, вероятно, помните из главы 2, у командной оболочки есть команда and, благодаря которой выполнение последующей команды возможно лишь при успешном завершении предыдущей. В данном примере вас не слишком заботит успешное завершение предыдущих команд, поэтому можно придерживаться более простой формы.

Если вы — обычный пользователь, то у вас может не быть прав на установку новых команд в каталог /usr/local/bin. Можно изменить в make-файле каталог установки, изменить права доступа к этому каталогу или заменить пользователя (с помощью команды su) на root перед запуском make install.

$ rm *.о myapp

$ make -f Makefile3

gcc -I. -g -Wall -ansi -c main.c

gcc -I. -g -Wall -ansi -c 2.c

gcc -I. -g -Wall -ansi -с 3.c

gcc -o myapp main.о 2.o 3.o

$ make -f Makefile3

make: Nothing to be done for 'all'.

$ rm myapp

$ make -f Makefile3 install

gcc -o myapp main.o 2.o 3.o

Installed in /usr/local/bin

make -f Makefile3 clean

rm main.о 2.о 3.о

$

Как это работает

Сначала вы удаляете файл myapp и все объектные файлы. Команда make самостоятельно выбирает задание all, которое приводит к сборке myapp. Затем вы снова запускаете команду make, но т.к. у файла myapp свежая версия, make не делает ничего. Далее вы удаляете файл myapp и выполняете make install. Эта команда создает заново двоичный файл и копирует его в каталог установки. В заключение выполняется команда make clean, удаляющая объектные файлы.

Встроенные правила

До сих пор вы описывали в make-файле подробно, как выполнять каждый шаг. В действительности у команды make есть много встроенных правил, которые существенно упрощают make-файлы, особенно если у вас много исходных файлов. Для того чтобы проверить это, создайте традиционную программу, приветствующую мир:

#include <stdlib.h>

#include <stdio.h>

int main() {

 printf("Hello World\n");

 exit(EXIT_SUCCESS);

}

He задавая make-файла, попробуйте откомпилировать ее с помощью команды make:

$ make foo

сс foo.с -о foo

$

Как видите, make знает, как запустить компилятор, хотя в данном случае она выбирает сс вместо gcc (в ОС Linux это нормально, потому что cc — обычно ссылка на gcc). Иногда эти встроенные правила называют подразумеваемыми правилами. Стандартные правила используют макросы, поэтому задавая некоторые новые значения для макросов, вы можете изменить стандартное поведение.

$ rm foo

$ make CC=gcc CFLAGS="-Wall -g" foo

gcc -Wall -g foo.с -o foo

$

С помощью опции -p можно попросить команду make вывести на экран встроенные правила. Их так много, что мы не можем привести в книге все встроенные правила, ограничимся коротким фрагментом вывода команды make -p версии GNU, демонстрирующим часть этих правил:

OUTPUT_OPTION = -o $@

COMPILE.с = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -с

%.о: %.с

# commands to execute (built-in) :

 $(COMPILE.с) $(OUTPUT_OPTION) $<

Теперь, принимая во внимание описанные встроенные правила, вы можете упростить ваш make-файл, удалив правила для создания объектных файлов и оставив только зависимости, таким образом, соответствующая секция make-файла читается просто:

main.о: main.c a.h

2.о: 2.с a.h b.h

3.o: 3.c b.h c.h

Эту версию можно найти в загружаемом из Интернета программном коде, в файле Makefile4.

Суффиксы и шаблоны правил

Встроенные правила, которые вы видели, действуют, используя суффиксы (подобные расширениям файлов в системах Windows и MS-DOS), поэтому команда make, получая файл с одним окончанием, знает, какое правило применять для создания файла с другим окончанием. Чаще всего используется правило для создания файла, заканчивающегося .о, из файла с окончанием .c. Это правило для применения компилятора, компилирующего исходный файл, но не компонующего.

Порой возникает потребность в создании новых правил. Авторы приучили себя работать с исходными файлами, которые необходимо компилировать несколькими разными компиляторами: двумя в среде MS-DOS и gcc в ОС Linux. Для того чтобы осчастливить один из компиляторов MS-DOS, исходные файлы на языке С++, а не С должны иметь расширение cpp. К сожалению, у версии команды make, применяемой в Linux, в то время не было встроенного правила для компиляции файлов с окончанием .cpp. (Существовало правило для суффикса .cc, более распространенного расширения файла на C++ в среде UNIX.)

Следовательно, нужно было либо задавать правило для каждого отдельного исходного файла, либо научить make новому правилу для создания объектных файлов из файлов с расширением cpp. Учитывая, что в том проекте было довольно большое количество исходных файлов, определение нового правила сэкономило бы много времени на наборе и существенно облегчило бы добавление новых исходных файлов в проект.

Для вставки правила с новым суффиксом сначала добавьте строку в make-файл, информирующую команду make о новом суффиксе; далее можно написать правило, используя новый суффикс. Команда make применяет специальную синтаксическую запись

.<old_suffix>.<new_suffix>:

для определения общего правила создания файлов с новым суффиксом из файлов с тем же основным именем, но старым суффиксом.

Далее приведен фрагмент make-файла с новым общим правилом для компиляции файлов с суффиксом .срр в файлы с суффиксом .о:

.SUFFIXES: .cpp

.cpp.o:

 $ (CC) -xc++ $(CFLAGS) -I$(INCLUDE) -с $<

Особая зависимость .cpp.o: информирует команду make о том, что следующие правила предназначены для трансляции файла с суффиксом .cpp в файлы с суффиксом .о. При написании этой зависимости применяются имена специальных макросов, поскольку неизвестны реальные имена файлов, которые будут транслироваться. Для того чтобы понять это правило, нужно просто вспомнить, что символы $< заменяются начальным именем файла (со старым суффиксом). Имейте в виду, что вы сообщили make только о том, как получить из файла с суффиксом .cpp файл с суффиксом .о; как из объектного файла получить двоичный исполняемый файл, команда make уже знает.

После запуска команда make применяет ваше новое правило для получения из файла bar.cpp файла bar.o; далее она использует свои встроенные правила для превращения файла с суффиксом .о в исполняемый файл. Дополнительный флаг -xc++ должен сообщить программе gcc о том, что она имеет дело с исходным файлом на языке C++.

В наши дни команда make знает, как работать с исходными файлами на С++ с расширениями cpp, но данный метод полезен для преобразования файла одного типа в файл другого типа.

Самые последние версии команды make включают в себя альтернативную синтаксическую запись для достижения того же эффекта и многое другое. Например, правила с шаблонами используют знак подстановки % для сопоставления имен файлов и не полагаются на одни лишь расширения этих имен.

Далее приведено правило с шаблоном, эквивалентное предыдущему правилу с суффиксом .cpp:

%.cpp: %o

 $(СС) -xc++ $(CFLAGS) -I$(INCLUDE) -с $<

Управление библиотеками с помощью make

Когда вы работаете над большими проектами, часто удобно управлять компиляцией нескольких программных единиц с помощью библиотеки. Библиотеки — это файлы, в соответствии с соглашением имеющие расширение a (archive) и содержащие коллекцию объектных файлов. Для работы с библиотеками у команды make есть специальная синтаксическая запись, которая существенно облегчает управление ими.

Синтаксическая запись lib(file.о) означает объектный файл file.o, хранящийся в библиотеке lib.а. У команды make есть встроенное правило для управления библиотеками, которое обычно эквивалентно приведенному далее фрагменту:

.с.а:

 $(CC) -с $(CFLAGS)

 $< $(AR) $(ARFLAGS) $@ $*.о

Макросы $(AR) и $(ARFLAGS) подразумевают команду ar и опции rv соответственно. Довольно краткая синтаксическая запись информирует команду make о том, что для включения файла .с в библиотеку .а следует применить два следующих правила:

□ первое правило говорит о том, что команда make должна откомпилировать исходный файл и сформировать объектный файл;

□ второе правило предписывает применить команду ar для модификации библиотеки, заключающейся в добавлении нового объектного файла.

Итак, если у вас есть библиотека fud, содержащая файл bas.o, в первом правиле $< заменяется именем bas.c. Во втором правиле $@ заменяется именем библиотеки fud.а и $* заменяется именем bas.

Выполните упражнение 9.4.

Упражнение 9.4. Управление библиотекой

Правила управления библиотеками очень просто применять на практике. В этом упражнении вы измените свое приложение, сохранив файлы 2.o и 3.o в библиотеке mylib.a. Make-файл потребует лишь нескольких изменений и его новый вариант Makefile5 будет выглядеть следующим образом:

all: myapp

# Какой компилятор

CC = gcc

# Куда установить

INSTDIR = /usr/local/bin

# Где хранятся файлы include

INCLUDE =

# Опции для разработки

CFLAGS = -g -Wall -ansi

# Опции для рабочей версии

# CFLAGS = -O -Wall -ansi

# Локальные библиотеки

MYLIB = mylib.a

myapp: main.o $(MYLIB)

 $(CC) -o myapp main.o $(MYLIB)

$(MYLIB): $(MYLIB)(2.o) $(MYLIB)(3.o)

main.o: main.c a.h

2.o: 2.c a.h b.h

3.o: 3.c b.h c.h

clean:

 -rm main.o 2.o 3.o $(MYLIB)

install: myapp

 @if [ -d $(INSTDIR) ]; \

 then \

  cp myapp $(INSTDIR);\

  chmod a+x $(INSTDIR)/myapp;\

  chmod og-w $(INSTDIR)/myapp;\

  echo "Installed in $(INSTDIR)";\

 else \

  echo "Sorry, $(INSTDIR) does not exist";\

 fi

Обратите внимание на то, как вы разрешили правилам по умолчанию выполнить большую часть работы. Теперь проверьте новую версию make-файла:

$ rm -f myapp *.o mylib.a

$ make -f Makefile5

gcc -g -Wall -ansi -с -o main.о main.c

gcc -g -Wall -ansi -с -o 2.о 2.c

ar rv mylib.a 2.o

a - 2.o

gcc -g -Wall -ansi -с -о 3.o 3.c

ar rv mylib.a 3.o

a - 3.о

gcc -o myapp main.о mylib.a

$ touch c.h

$ make -f Makefile5

gcc -g -Wall -ansi -с -о 3.o 3.c

ar rv mylib.a 3.o

r - 3.о

gcc -o myapp main.о mylib.a

$

Как это работает

Сначала вы удаляете все объектные файлы и библиотеку и разрешаете команде make создать файл myapp, что она и делает, откомпилировав и создав библиотеку перед тем, как компоновать файл main.о с библиотекой для создания исполняемого файла myapp. Далее вы тестируете зависимость для файла 3.o, которая информирует команду make о том, что, если меняется файл c.h, файл 3.c следует заново откомпилировать. Она делает это корректно, откомпилировав файл и обновив библиотеку перед перекомпоновкой, создающей новую версию исполняемого файла myapp.

Более сложная тема: make-файлы и подкаталоги

При работе над большими проектами порой бывает удобно отделить от основных файлов файлы, формирующие библиотеку, и поместить их в подкаталог. С помощью команды make можно сделать это двумя способами.

Во-первых, можно создать в подкаталоге второй make-файл для компиляции файлов, сохранения их в библиотеке и последующего копирования библиотеки на уровень вверх, в основной каталог. При этом в основном make-файле, хранящемся в каталоге более высокого уровня, есть правило формирования библиотеки, в котором описан запуск второго make-файла следующим образом:

mylib.a:

 (cd mylibdirectory;$(MAKE))

Это правило гласит, что вы всегда должны пытаться создать mylib.a с помощью команды make. Когда make инициирует правило создания библиотеки, она изменяет каталог на mylibdirectory и затем запускает новую команду make для управления библиотекой. Поскольку для этого запускается новая командная оболочка, программа, применяющая make-файл, не выполняет команду cd. А командная оболочка, запущенная для выполнения правила построения библиотеки, находится в другом каталоге. Скобки обеспечивают выполнение всего процесса в одной командной оболочке.

Второй способ заключается в применении нескольких макросов в одном make-файле. Дополнительные макросы генерируются добавлением символа D для каталога или символа F для имени файла к тем макросам, которые мы уже обсуждали. Вы можете переписать встроенное правило с суффиксами .с.о

.c.o:

 $(СС) $(CFLAGS) -с $(@D)/$(<F) -о $(@D)/$(@F)

для компиляции файлов в подкаталоге и сохранения в нем объектных файлов. Затем вы обновляете библиотеку в текущем каталоге с помощью зависимости и правила, наподобие приведенных далее:

mylib.a: mydir/2.o mydir/3.о

 ar -rv mylib.a $?

Вы должны решить, какой из способов предпочтительнее в вашем проекте. Многие проекты просто избегают применения подкаталогов, но это может привести к непомерному разрастанию исходного каталога. Как видно из только что приведенного краткого обзора, команду make можно использовать с подкаталогами и сложность возрастает при этом лишь незначительно.

Версия GNU команд make и gcc

Для GNU-команды make и GNU-компилятора gcc существуют две интересные дополнительные опции.

□ Первая — опция -jN ("jobs") команды make. Она позволяет make выполнять N команд одновременно. Там, где несколько разных частей проекта могут компилироваться независимо, команда make запускает несколько правил в одно и то же время. В зависимости от конфигурации вашей системы эта возможность может существенно сократить время, затраченное на перекомпиляцию. Если у вас много исходных файлов, может быть стоит воспользоваться этой опцией. Как правило, небольшие числа, например -j3, — хорошая отправная точка. Если вы делите компьютер с другими пользователями, применяйте эту опцию с осторожностью. Другие пользователи могут не одобрить запуск большого количества процессов при каждой вашей компиляции!

□ Второе полезное дополнение — опция -MM для gcc. Она формирует список зависимостей в форме, подходящей для команды make. В проекте со значительным числом исходных файлов, каждый из которых содержит разные комбинации заголовочных файлов, бывает трудно (но крайне важно) корректно определить зависимости. Если сделать каждый исходный файл зависимым от всех заголовочных файлов, иногда вы будете компилировать файлы напрасно. С другой стороны, если вы пропустите какие-то зависимости, возникнет еще более серьезная проблема, поскольку в этом случае вы не откомпилируете заново те файлы, которые нуждаются в перекомпиляции.

Выполните упражнение 9.5.

Упражнение 9.5. Использование gcc -MM

В этом упражнении вы примените опцию -MM в программе gcc для генерации списка зависимостей вашего примера:

$ gcc -MM main.с 2.с 3.с

main.о: main.c a.h

2.о: 2.с a.h b.h

3.o: 3.с b.h c.h

$

Как это работает

Компилятор gcc просто просматривает исходные файлы, ищет заголовочные файлы и выводит требующиеся строки зависимостей в формате, готовом к вставке в make- файл. Вы должны лишь сохранить вывод во временном файле и затем вставить его в make-файл, чтобы иметь безошибочный набор зависимостей. Если вы пользуетесь копией, полученной от gcc, для появления ошибок в ваших зависимостях просто нет оснований!

Если вы хорошо знакомы с make-файлами, можно попробовать применить средство makedepend, которое выполняет функцию, аналогичную опции -MM, но вставляет полученный список зависимостей в конец реального заданного вами make-файла.

Перед завершением темы make-файлов, быть может, стоит подчеркнуть, что не следует ограничивать применение make-файлов только компиляцией кода и созданием библиотек. Их можно использовать для автоматизации любой задачи, в которой есть последовательность команд, формирующих из входного файла некоторого типа выходной файл. Типичным "некомпиляционным" применением может быть вызов программ awk, или sed для обработки некоторых файлов или генерация интерактивного справочного руководства. Вы можете автоматизировать практически любую обработку файлов, если на основании информации о дате и времени модификации файла можете определить, какие из файлов были изменены.

Другая возможность управления вашими сборками или на самом деле другой способ автоматизации задач — утилита ANT. Это средство на базе языка Java, использующее файлы конфигурации, написанные на языке XML. Ее обычно не применяют в ОС Linux для автоматизации создания исполняемых файлов из файлов на языке С, поэтому мы не будем обсуждать ее в книге. Более подробную информацию об ANT можно найти на Web-сайте http://ant.apache.org/.

Управление исходным кодом

Если вы ушли от простых проектов и особенно если несколько человек работает над проектом, управление изменениями, вносимыми в исходные файлы, становится важной составляющей, которая позволяет избежать конфликтных корректировок и отслеживать сделанные изменения.

В среде UNIX есть несколько широко распространенных систем управления исходными файлами:

□ SCCS (Source Code Control System);

□ RCS (Revision Control System);

□ CVS (Concurrent Version System);

□ Subversion.

SCCS первоначально была системой управления исходным кодом, введенной компанией AT&T в версии System V ОС UNIX, а сейчас она — часть стандарта X/Open. RCS была разработана позже как бесплатная замена SCCS и распространяется Фондом бесплатного программного обеспечения (Free Software foundation). RCS функционально очень похожа на SCCS, но с интуитивно более понятным интерфейсом и некоторыми дополнительными опциями, поэтому система SCCS по большей части вытеснена RCS.

Утилиты RCS обычно включены в дистрибутивы Linux или же их можно загрузить из Интернета вместе с исходными файлами с Web-сайта Фонда бесплатного программного обеспечения со страницы http://directory.fsf.org/rcs.html.

CVS — более передовая, чем SCCS или RCS, система, которая может быть инструментом для совместных разработок на базе Интернета. Ее можно найти в большинстве дистрибутивов Linux или по адресу http://www.nongnu.org/cvs/. У этой системы два существенных преимущества по сравнению с RCS: ее можно применять в сетевых соединениях и она допускает параллельные разработки.

Subversion — новое детище, входящее в блок, проектируемый для замены системы CVS когда-нибудь в будущем. Начальную страницу Web-сайта этой системы можно найти по адресу http://www.subversion.org.

В этой главе мы сосредоточимся на системах RCS и CVS; выбор RCS объясняется легкостью ее использования в индивидуальных проектах, хорошей интегрированностью с командой make, a CVS выбрана потому, что это самая популярная форма управления исходным кодом, применяемая в совместных проектах. Мы также кратко сравним команды RCS с командами SCCS, поскольку последняя обладает статусом стандарта POSIX, и некоторые пользовательские команды CVS с командами системы Subversion.

RCS

Revision Control System (RCS, система управления версиями) содержит ряд команд для управления исходными файлами. Она функционирует, отслеживая исходный файл по мере его изменения и сохраняя единый файл со списком изменений, достаточно подробным для того, чтобы можно было воссоздать любую предыдущую версию файла. Система также позволяет хранить комментарии, связанные с каждым изменением, которые могут оказаться полезными, если вы оглядываетесь назад, изучая хронологию изменений файла.

По мере продвижения проекта вы можете регистрировать в файле отдельно каждое значительное изменение или исправление ошибки и сохранять комментарии к каждому изменению. Это может оказаться очень полезным при просмотре изменений, внесенных в файл, проверке фрагментов с исправленными ошибками, и иногда возможно и внесенными ошибками!

Поскольку RCS сохраняет только различия между версиями, она эффективно использует дисковое пространство. Кроме того, система позволяет получить предыдущие версии в случае ошибочного удаления.

Команда rcs

Для иллюстрации сказанного начните с начальной версии файла, которым хотите управлять. В данном случае давайте использовать файл important.c, который начинает существование как копия файла foo.с со следующим комментарием, добавленным в начало файла:

/*

 Это важный файл для управления данным проектом.

 В нем реализована каноническая программа "Hello World".

*/

Первая задача — инициализировать RCS-контроль над файлом с помощью команды rcs. Команда rcs -i инициализирует файл RCS-управления.

$ rcs -i important.с

RCS file: important.с,v

enter description, terminated with single '.' or end of file:

NOTE: This is NOT the log message!

>> This is an important demonstration file

>> .

done

$

Разрешается применять множественные строки комментариев. Завершите строку приглашения одиночной точкой (.) в самой строке или набрав символ конца файла, обычно комбинацией клавиш <Ctrl>+<D>.

После этой команды rcs создается новый предназначенный только для чтения (read-only) файл с расширением v.

$ ls -l

-rw-r--r-- 1 neil users 225 2007-07-09 07:52 important.c

-r--r--r-- 1 neil users 105 2007-07-09 07:52 important.с,v

$

Примечание

Если вы предпочитаете сохранять RCS-файлы в отдельном каталоге, просто создайте подкаталог с именем RCS перед первым применением команды rcs. Все команды rcs будут автоматически использовать подкаталог RCS для RCS-файлов.

Команда сi

Теперь вы можете выполнить регистрируемый ввод в RCS-файл (check-in) вашего файла с помощью команды ci для сохранения его текущей версии.

$ ci important.с

important.c,v <-- important.c

initial revision: 1.1

done

$

Если вы забыли выполнить первой команду rcs -i, RCS запросит описание файла. Если теперь заглянуть в каталог, то можно увидеть, что файл important.c удален.

$ ls -l

-r--r--r-- 1 neil users 443 2007-07-07 07:54 important.с,v

$

Содержимое файла и управляющая информация хранятся в RCS-файле important.c,v.

Команда со

Если вы хотите изменить файл, прежде всего его надо извлечь (check out). Если нужно просто прочитать файл, можно применить команду со для повторного создания текущей версии файла и изменения ее прав доступа на read-only (только чтение). Если же файл нужно редактировать, следует заблокировать файл с помощью команды со -l. Причина заключается в том, что в командных проектах важно быть уверенным в том, что в определенный момент времени только один человек корректирует данный файл. Вот почему только у одной копии данной версии файла есть право на запись. Когда файл извлечен в каталог с правом на запись, RCS-файл блокируется. Заблокируйте копию файла

$ со -l important.c

important.с,v --> important.c

revision 1.1 (locked) done

$

и загляните в каталог:

$ ls -l

-rw-r--r-- 1 neil users 225 2007-07-09 07:55 important.c

-r--r--r-- 1 neil users 453 2007-07-09 07:55 important.с,v

$

Теперь у вас появился файл для редактирования и внесения новых изменений. Выполните корректировку, сохраните новую версию и используйте команду ci еще раз для сохранения изменений. Секция вывода в файле important.c теперь следующая:

printf("Hello World\n");

printf("This is an extra line added later\n");

Примените ci следующим образом:

$ ci important.с

important.с,v <-- important.c

new revision: 1.2;

previous revision: 1.1

enter log message, terminated with single or end of file:

>> Added an extra line to be printed out.

>> .

done

$

Примечание

Для записи изменений и сохранения блокировки, разрешающей пользователю продолжить работу с файлом, следует вызвать команду ci с опцией -l. Файл будет автоматически снова извлечен в каталог для того же самого пользователя.

Вы сохранили обновленную версию файла. Если сейчас заглянуть в каталог, можно увидеть, что файл important.c снова удален.

$ ls -l

-r--r--r-- 1 neil users 635 2007-07-09 07:55 important.с,v

$

Команда rlog

Часто бывает полезно просмотреть сводку изменений, внесенных в файл. Сделать это можно с помощью команды rlog.

$ rlog important.с

RCS file: important.c,v

Working file: important.c

head: 1.2

branch:

locks: strict

access list:

symbolic names:

keyword substitution: kv

total revisions: 2; selected revisions: 2

description:

This is an important demonstration file

------------------------

revision 1.2

date: 2007/07/09 06:57:33; author: neil; state: Exp; lines: +1 -0

Added an extra line to be printed out.

------------------------

revision 1.1

date: 2007/07/09 06:54:36; author: neil; state: Exp;

Initial revision

==================================================================

$

В первой части дается описание файла и опций, используемых командой rcs. Далее команда rlog перечисляет версии файла, начиная с самой свежей, вместе с текстом, который вы вводите при сохранении версии. lines:+1-0 в версии 1.2 информирует вас о том, что была вставлена одна строка и ни одна строка не была удалена.

Примечание

Учтите, что время модификации файла записывается без учета летнего времени, чтобы избежать проблем при переводе часов.

Теперь, если вы хотите вернуть первую версию файла, можно запросить команду со, указав нужную версию.

$ со -r1.1 important.c

important.с,v --> important.c

revision 1.1

done

$

У команды ci тоже есть опция -r, которая присваивает номеру версии заданное значение. Например, команда

ci -r2 important.c

сохранит файл important.c как версию 2.1. Обе системы, RCS и SCCS, по умолчанию используют 1 как наименьший номер версии.

Команда rcsdiff

Если вы хотите знать, чем отличаются две версии, можно применить команду rcsdiff:

$ rcsdiff -r1.1 -r1.2 important.c

=================================================

RCS file: important.c,v

retrieving revision 1.1

retrieving revision 1.2

diff -r1.1 -r1.2

11a12

> printf("This is an extra line added later\n");

$

Вывод информирует вас о том, что была добавлена одна строка после исходной строки 11.

Обозначение версий

Система RCS может применять специальные строки (макросы) внутри исходного файла, помогающие отслеживать изменения. Наиболее популярны два макроса: $RCSfile$ и $Id$. Макрос $RCSfile$ замещается именем файла, а макрос $Id$ — строкой, обозначающей версию. Полный список поддерживаемых специальных строк можно найти в интерактивном руководстве. Макросы замещаются, когда версия файла извлекается из RCS-файла, и обновляются автоматически, когда версия регистрируется и сохраняется в RCS-файле.

Давайте в третий раз изменим файл и добавим несколько таких макросов:

$ со -l important.с

important.c,v --> important.с

revision 1.2 (locked)

done

$

Отредактируйте файл в соответствии с приведенным далее кодом:

#include <stdlib.h>

#include <stdio.h>

/*

 Это важный файл для управления данным проектом.

 В нем реализована каноническая программа "Hello World".

 Filename: $RCSfile$

*/

static char *RCSinfo = "$Id$";

int main() {

 printf ("Hello World\n");

 printf("This is an extra line added later\n");

 printf("This file is under RCS control. Its ID is\n%s\n", RCSinfo);

 exit(EXIT_SUCCESS);

}

Теперь сохраните эту версию и посмотрите, как RCS управляет специальными строками:

$ ci important.с

important.с,v <-- important.c

new revision: 1.3;

previous revision: 1.2

enter log message, terminated with single '.' or end of file:

>> Added $RCSfile$ and $Id$ strings

>> .

done

$

Если заглянете в каталог, то найдете только RCS-файл.

$ ls -l

-r--r--r-- 1 neil users 907 2007-07-09 08:07 important.с,v

$

Если вы извлечете текущую версию исходного файла (с помощью команды со) и просмотрите его, то увидите, что макросы раскрыты:

#include <stdlib.h>

#include <stdio.h>

/*

 Это важный файл для управления данным проектом.

 В нем реализована каноническая программа "Hello World".

 Filename: $RCSfile: important.с,v $

*/

static char *RCSinfo = "$Id: important.c,v 1.3 2007/07/09. 07:07:08 neil Exp $";

int main() {

 printf("Hello World\n");

 printf("This is an extra line added later\n");

 printf("This file is under RCS control. Its ID is\n%s\n", RCSinfo);

 exit(EXIT_SUCCESS);

}

А теперь выполните упражнение 9.6.

Упражнение 9.6. GNU-версия make с RCS

У команды make версии GNU есть несколько встроенных правил для управления RCS-файлами. В этом примере вы увидите, как работать с отсутствующим исходным файлом.

$ rm -f important.с

$ make important

со important.с,v important.c

important.с,v --> important.c

revision 1.3

done

сс -c important.c -o important.о сс important.о -о important

rm important.о important.с

$

Как это работает

У команды make есть стандартное правило для создания файла без расширения с помощью компиляции файла с тем же именем и расширением с. Второе стандартное правило разрешает make создать файл important.c из файла important.c,v, используя RCS. Поскольку нет файла с именем important.c, команда make создала файл с расширением с, получив последнюю версию файла с помощью команды со. После компиляции она навела порядок, удалив файл important.c.

Команда ident

Команду ident можно применять для поиска версии файла, которая содержит строку $Id$. Поскольку вы сохранили строку в переменной, она появляется и в результирующем исполняемом файле. Может оказаться так, что, если вы включили специальные строки в исходный код, но никогда не обращаетесь к ним, компилятор из соображений оптимизации удалит их. Эту проблему можно обойти, добавив в исходный код несколько фиктивных обращений к ним, хотя по мере улучшения компиляторов делать это становится все труднее!

Далее показан простой пример того, как можно использовать команду ident для двойной проверки RCS-версии исходного файла, применяемого для формирования исполняемого файла (упражнение 9.7).

Упражнение 9.7. Команда ident

$ ./important

Hello World

This is an extra line added later

This file is under RCS control. Its ID is

$Id: important.c,v 1.3 2007/07/09 07:07:08 neil Exp $

$ ident important

important:

$Id: important.c,v 1.3 2007/07/09 07 :07 :08 neil Exp $

$

Как это работает

Выполняя программу, вы показываете строку, включенную в исполняемый файл. Далее вы демонстрируете, как команда ident может извлечь из исполняемого файла строки вида $Id$.

Этот метод применения RCS и строк вида $Id$, включаемых в исполняемые файлы, может быть очень мощным средством определения версии файла, содержащей ошибку, о которой сообщил пользователь. RCS-файлы (или SCCS) можно применять как часть средства отслеживания в проекте проблем, о которых сообщается, и способов их устранения. Если вы продаете программное обеспечение или даже отдаете его бесплатно, очень важно знать, что изменилось между двумя выпущенными версиями.

Если вас интересует дополнительная информация, на странице rcsintro интерактивного руководства в дополнение к стандартному руководству по RCS приведено введение в систему RCS. В него также включены страницы, посвященные отдельным командам, таким как ci, со и т.д.

SCCS

Система SCCS предлагает средства, очень похожие на средства системы RCS. Преимущество системы SCCS лишь в том, что она определена в стандарте X/Open, поэтому все версии UNIX известных производителей должны ее поддерживать. С практической точки зрения система RCS предпочтительнее, она легко переносится на разные платформы и распространяется бесплатно. Поэтому, если у вас UNIX-подобная система, независимо от ее отображения на стандарт X/Open, вы сможете получить для нее и установить в ней систему RCS. По этой причине мы не будем описывать далее в книге систему SCCS, лишь приведем краткое сравнение команд, имеющих аналоги в обеих системах.

Сравнение RCS и SCCS

Трудно провести прямую аналогию между командами двух систем, поэтому табл. 9.2 следует рассматривать как краткий указатель. У команд, перечисленных в таблице, разные опции для выполнения одних и тех же задач. Если вы должны применять систему SCCS, следует найти соответствующие опции, но, по крайней мере, вы будете знать, где их искать.

Таблица 9.2

RCS SCCS
rcs admin
ci delta
со get
rcsdiff sccsdiff
ident what

В дополнение к только что перечисленным командам у команды sссs одноименной системы есть некоторое пересечение с командами rcs и со системы RCS. Например, команды sссs edit и sссs create эквивалентны командам со -l и rcs -i соответственно.

CVS

Альтернатива применения системы RCS для управления изменениями в файлах — система CVS, которая означает Concurrent Versions System (система параллельных версий). CVS стала очень популярной, может быть, потому, что у нее есть одно явное преимущество по сравнению с системой RCS: на практике CVS используется в Интернете, а не только в совместно используемом локальном каталоге как RCS. В системе CVS также разрешены параллельные разработки, т. е. многие программисты могут работать с одним и тем же файлом одновременно, в то время как RCS разрешает лишь одному пользователю работать с конкретным файлом в определенный момент времени. Команды CVS похожи на команды RCS, поскольку первоначально CVS была разработана как внешний интерфейс RCS.

Поскольку она способна гибко функционировать в сети, система CVS подходит для применения в тех случаях, когда у разработчиков есть единственная связь — через Интернет. Многие проекты Linux и GNU используют систему CVS, чтобы помочь разработчикам координировать их работу. В основном использование CVS для управления удаленными файлами аналогично применению системы для обработки локальных файлов.

В этой главе мы кратко рассмотрим основы системы CVS, чтобы вы могли начать работать с локальными репозитариями и понимали, как получить копию самых свежих исходных файлов проекта, если сервер CVS доступен в Интернете. Дополнительная информация хранится в руководстве по CVS, написанном Пером Седерквистом (Per Cederqvist) и др. и доступном по адресу http://ximbiot.com/cvs/manual/, там вы найдете файлы FAQ (часто задаваемые вопросы) и другие полезные файлы.

Прежде всего, вы должны создать репозитарий или хранилище, в котором CVS будет хранить свои управляющие файлы и основные копии файлов, управляемые ею. У репозитария древовидная структура, поэтому можно применять один репозитарий для хранения целиком всей структуры каталогов, предназначенных не только для одного проекта, но и для нескольких проектов. Для несвязанных проектов можно использовать и отдельные репозитарий. В следующих разделах вы увидите, как сообщить системе CVS о том, какой репозитарий применять.

Локальное использование CVS

Начните с создания репозитария. Для простоты пусть это будет локальный репозитарий, и поскольку вы будете использовать только один, удобно поместить его в каталог /usr/local. В большинстве дистрибутивов Linux все обычные пользователи являются членами группы users, поэтому примените ее как группу репозитария, чтобы у всех пользователей был доступ к нему.

Как суперпользователь создайте каталог для репозитария:

# mkdir /usr/local/repository

# chgrp users /usr/local/repository

# chmod g+w /usr/local/repository

И превратившись снова в обычного пользователя, инициализируйте его как репозитарий CVS. У вас должно быть право на запись в каталог usr/local/repository, если вы не входите в группу обычных пользователей.

$ cvs -d /usr/local/repository init

Опция -d информирует CVS о том, где вы хотите создать репозитарий.

После создания репозитария можно сохранить начальные версии файлов проекта в системе CVS. Но в этот момент можно сэкономить на наборе. У всех команд cvs есть два способа поиска каталога системы CVS. Во-первых, они ищут опцию -d <путь> в командной строке (как и в команде init), если опций -d нет, ищется переменная окружения CVSROOT. Вместо постоянного применения опции -а вы задаете переменную окружения. Приведенную далее команду можно использовать, если в качестве командной оболочки вы применяете bash:

$ export CVSROOT=/usr/local/repository

Прежде всего, вы изменяете каталог, в котором находится проект; далее вы сообщаете CVS о необходимости импортировать все файлы проекта в этот каталог. Для системы CVS проект — это любая коллекция связанных файлов и каталогов. Обычно она включает все файлы, необходимые для создания приложения. Термин "импорт" означает передачу всех файлов под контроль системы CVS и копирование их в CVS-репозитарий. В данном примере у вас есть каталог cvs-sp (простой проект CVS), содержащий два файла — hello.c и Makefile.

$ cd cvs-sp

$ ls -l

-rw-r--r-- 1 neil users 68  2003-02-15 11:07 Makefile

-rw-r--r-- 1 neil users 109 2003-02-15 11:04 hello.c

Команда импорта в CVS (cvs import) применяется следующим образом: 

$ cvs import -m"Initial version of Simple Project" wrox/chap9-cvs wrox start

Это заклинание заставляет CVS импортировать все файлы в текущий каталог (cvs-sp) и передает системе регистрационное сообщение (log message).

Аргумент wrox/chap9-cvs информирует CVS о том, где относительно корня дерева CVS сохранять новый проект. Напоминаем, что при желании в одном репозитарии можно хранить несколько проектов. Параметр wrox — тег поставщика, применяемый для идентификации автора первоначальной версии импортируемых файлов, а start — тег версии. Теги версии можно применять для идентификации в виде группы наборов связанных файлов, создающих конкретную версию приложения. Система CVS отвечает строками

N wrox/chap9-cvs/hello.c

N wrox/chap9-cvs/Makefile

Nо conflicts created by this import

информируя вас о том, что два файла импортированы корректно.

Сейчас самое время проверить возможность извлечения ваших файлов из системы CVS. Вы можете создать каталог junk и вернуть в него файлы, чтобы убедиться в том, что все нормально:

$ mkdir junk

$ cd junk

$ cvs checkout wrox/chap9-cvs

U wrox/chap9-cvs/Makefile

U wrox/chap9-cvs/hello.с

Вы указываете CVS тот же путь, что и при копировании файлов в репозитарий. Система CVS создает в текущем каталоге каталог wrox/chap9-cvs и помещает туда файлы.

Теперь вы готовы внести некоторые изменения в ваш проект. Отредактируйте файл hello.c в каталоге wrox/chap9-cvs, вставив в него строку

printf("Have a nice day\n");

Затем откомпилируйте заново и выполните программу, чтобы убедиться в том, что все в порядке:

$ make

сс hello.c -о hello

$ ./hello

Hello World

Have a nice day

$

Вы можете спросить у системы CVS о том, что изменилось в проекте. Не нужно сообщать CVS, какой именно файл вас интересует, она может работать со всем каталогом одновременно.

$ cvs diff

CVS отвечает следующими строками:

cvs diff: Diffing

Index: hello.c

========================================================

RCS file: /usr/local/repository/wrox/chap9-cvs/hello.c,v

retrieving revision 1.1.1.1

diff -r1.1.1.1 hello.c

6a7

> printf("Have a nice day\n");

Вы довольны внесенным изменением и хотите зафиксировать его в CVS.

Когда вы фиксируете изменение с помощью системы CVS, она запускает редактор, позволяющий вам ввести регистрационное сообщение. У вас есть возможность задать переменную окружения CVSEDITOR для запуска определенного редактора перед выполнением команды commit:

$ cvs commit

CVS сообщает о том, что она сохраняет:

cvs commit: Examining

Checking in hello.c;

/usr/local/repository/wrox/chap9-cvs/hello.c,v <-- hello.c

new revision: 1.2; previous revision: 1.1

done

Теперь вы можете запросить систему CVS об изменениях в проекте со времени его первого сохранения в репозитарии. Запросите набор изменений в каталоге wrox/chap9-cvs, начиная с версии 1.1 (начальная версия):

$ cvs rdiff -r1.1 wrox/chap9-cvs

Система CVS сообщает следующие подробности:

cvs rdiff: Diffing wrox/chap9-cvs

Index: wrox/chap9-cvs/hello.c

diff -с wrox/chap9-cvs/hello.с:1.1 wrox/chap9-cvs/hello.с:1.2

*** wrox/chap9-cvs/hello.с:1.1 Mon Jul 9 09:37:13 2007

--- wrox/chap9-cvs/hello.с Mon Jul 9 09:44:36 2007

************

*** 4,8 ****

--- 4,9 ---

int main() {

 printf("Hello World\n");

+ printf("Have a nice day\n");

 exit (EXIT_SUCCESS);

}

Предположим, что у вас есть копия, извлеченная из системы CVS в локальный каталог на время, и вы хотите обновить файлы в вашем локальном каталоге, которые корректировались другими пользователями, а вы сами их не редактировали. CVS может сделать это для вас, применив команду update. Перейдите на верхний уровень пути, в данном случае в каталог, содержащий каталог wrox, и выполните следующую команду:

$ cvs update -Pd wrox/chap9-cv3

CVS обновит файлы, извлекая из репозитария файлы, измененные другими пользователями, а не вами, и помещая их в ваш локальный каталог. Конечно, некоторые изменения могут оказаться несовместимыми с вашими, но это проблема, над которой вам придется потрудиться. Система CVS хороша, но она не умеет творить чудеса!

К этому моменту вы уже увидели, что использование CVS очень похоже на применение RCS. Но у нее есть существенное отличие, о котором мы пока не. упоминали, — способность функционировать в сети без смонтированной файловой системы.

Доступ к CVS по сети

Вы сообщили системе CVS, где находится репозитарий, применяя опцию -d в каждой команде или установив переменную окружения CVSROOT. Если вы хотите действовать через сеть, то просто используете расширенную синтаксическую запись для этого параметра. Например, во время написания книги все исходные файлы разработки GNOME (GNU Network Object Model Environment, сетевая объектная среда GNU — популярная графическая настольная система с открытым исходным кодом) были доступны в Интернете благодаря системе CVS. Вам нужно только задать месторасположение подходящего CVS-репозитария, указав некоторую сетевую информацию перед спецификатором пути к нему.

Другим примером может служить указание на CVS-репозитарий Web-стандартов консорциума W3C, значение переменной CVSROOT при этом должно быть равно :pserver:anonymous@dev.w3.org:/sources/public. Оно информирует систему CVS о том, что для доступа к репозитарию применяется аутентификация (pserver) с паролем и что репозитарий находится на сервере по адресу dev.w3.org.

Прежде чем получить доступ к исходным файлам, следует зарегистрироваться следующим образом:

$ export CVSROOT=:pserver:anonymous@dev.w3.org:/sources/public

$ cvs login

Когда появится приглашение для ввода пароля, введите anonymous.

Теперь вы готовы применять команды cvs во многом так же, как при работе с локальным каталогом, за исключением того, что следует добавлять опцию -z3 ко всем командам cvs, чтобы добиться сжатия для экономии полосы пропускания сети.

Если вы хотите получить исходные файлы HTML-валидатора W3C (системы проверки допустимости HTML-файлов), наберите приведенную далее команду:

$ cvs -z3 checkout validator

Если хотите сделать доступным в сети собственный репозитарий, необходимо запустить CVS-сервер на своей машине. Сделать это следует с помощью супердемона xinetd или inetd в зависимости от конфигурации вашей ОС Linux. Для применения хinetd отредактируйте файл /etp/xinetd.d/cvs, указав в нем местоположение CVS-репозитария, и воспользуйтесь средством настройки системы для активизации и запуска сервиса cvs. Для применения супердемона inetd просто добавьте строку в файл etc/inetd.conf и перезапустите inetd. Далее приведена необходимая строка:

2401 stream tcp nowait root /usr/bin/cvs cvs -b /usr/bin --allow-root=/usr/local/repository pserver

Она информирует inetd об автоматическом запуске CVS-сеанса для клиентов, подключающихся к порту 2401, стандартному порту CVS-сервера. Дополнительную информацию о запуске сетевых сервисов с помощью супердемона inetd см. в интерактивном справочном руководстве к inetd и inetd.conf.

Для использования системы CVS с вашим репозитарием и сетевым доступом к нему вы должны задать соответствующее значение переменной окружения CVSROOT. Например,

$ export CVSFOOT=:pserver:neil@localhost:/usr/local/repository

В этом коротком разделе мы смогли дать лишь поверхностное описание функциональных возможностей системы CVS. Если вы хотите основательно познакомиться с этой системой, настоятельно рекомендуем установить локальный репозитарий для экспериментов, найти расширенную документацию по CVS и получать удовольствие! Помните, что это система с открытым кодом, поэтому, если вы столкнулись с непонятными действиями программы или (что невероятно, но возможно) думаете, что обнаружили ошибку, всегда можно получить исходный программный код и изучить его самостоятельно. Начальная страница CVS расположена по адресу http://ximbiot.com/cvs/cvshome/.

Внешние интерфейсы CVS

Для доступа к CVS-репозитариям существует множество графических внешних интерфейсов. Может быть, их лучшую коллекцию для разных операционных систем можно найти на Web-сайте http://www.wincvs.org/. Там есть клиентское программное обеспечение для ОС Windows, Macintosh и, конечно, Linux.

Клиентская часть CVS позволяет создавать репозитарий и управлять им, включая удаленный доступ к репозитариям по сети.

На рис. 9.1 показана хронология работы с нашим простым приложением, отображенная WinCVS на сетевом клиенте под управлением ОС Windows.

Рис. 9.1

Subversion

Subversion разработана как система управления версиями, представляющая собой отличную замену системы CVS в сообществе разработчиков и пользователей программного обеспечения с открытым исходным кодом. Она проектировалась как "улучшенная CVS", о чем говорится на исходной странице Subversion Web-сайта http://subversion.tigris.org/, и, следовательно, обладает большей частью функциональных возможностей системы CVS и очень похожим работающим интерфейсом.

Популярность Subversion определенно растет, особенно в среде совместно разрабатываемых проектов, в которых над созданием приложения многие программисты работают вместе в Интернете. Большинство пользователей Subversion подключаются к сетевому репозитарию, настроенному менеджерами, разрабатываемого проекта. Эта система не так широко используется для управления индивидуальными или небольшими групповыми проектами, для них система CVS все еще остается основным применяемым средством.

В табл. 9.3 сравниваются основные команды в системах CVS и Subversion, эквивалентные друг другу.

Таблица 9.3

CVS Subversion
cvs -d /usr/local/repository init svnadmin create /usr/local/repository
cvs import wrox/chap9-cvs svn import cvs-sp file:///usr/local/repository/trunk
cvs checkout wrox/chap9-cvs svn checkout file:///usr/local/repository/trunk cvs-sp
cvs diff svn diff
cvs rdiff svn diff tag1 tag2
cvs update svn status -u
cvs commit svn commit

Полную документацию системы Subversion см. в интерактивной книге "Version Control with Subversion" ("Управление версиями с помощью Subversion") на Web-сайте http://svnbook.red-bean.com/.

Написание интерактивного справочного руководства

Если вы как часть задачи разрабатываете новую команду, вам следует создать интерактивное справочное руководство, описывающее ее работу. Как вы уже вероятно заметили, макет большинства страниц такого руководства следует жестко заданному шаблону следующего вида:

□ Header (Заголовок);

□ Name (Имя);

□ Synopsis (Краткий обзор);

□ Description (Описание);

□ Options (Опции);

□ Files (Файлы);

□ See also (См. также);

□ Bugs (Ошибки).

Вы можете пропустить разделы, которые не важны. Часто в конце справочного руководства появляется раздел "Author" (Автор).

Страницы справочного руководства в системах UNIX форматируются утилитой nroff или, как в большинстве систем Linux эквивалентом проекта GNU, утилитой groff. Обе они — разработки на основе более ранней команды roff или run-off. Вход утилиты nroff или groff — обычный текстовый файл за исключением того, что на первый взгляд синтаксис его непостижимо труден.

Без паники! Самый легкий способ написания новой программы в среде UNIX — начать с уже имеющейся программы и приспособить ее для своих целей, так же нужно поступать и с интерактивным справочным руководством.

В задачу данной книги не входит подробное объяснение множества опций, команд и макросов, которые может применять команда groff (или nroff). Вместо этого мы представляем простой шаблон, который вы можете позаимствовать и переделать в интерактивное справочное руководство для своего приложения.

Далее приведен исходный код страницы справочного руководства для приложения myapp, хранящийся в файле myapp.1.

.TH MYAPP 1

.SH NAME

Myapp \- A simple demonstration application that does very little.

.SH SYNOPSIS

.В myapp

[\-option ...]

.SH DESCRIPTION

.PP

\fImyapp\fP is a complete application that does nothing useful.

.PP

It was written for demonstration purposes.

.SH OPTIONS

.PP

It doesn't have any, but let's pretend, to make this template complete:

.TP

.BI \-option

If there was an option, it would not be -option.

.SH RESOURCES

.PP

myapp uses almost no resources.

.SR DIAGNOSTICS

The program shouldn't output anything, so if you find it doing so there's

probably something wrong. The return value is zero.

.SH SEE ALSO

The only other program we know with this little functionality is the

ubiquitous hello world application.

.SH COPYRIGHT

myapp is Copyright (c) 2007 Wiley Publishing, Inc.

This program is, free software; you can redistribute it and/or modify

it under the terms of the GNU General Public License as published by

the Free Software Foundation; either version 2 of the License, or

(at your option) any later version.

This program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.

You should have received a copy of the GNU General Public License

along, with this program; if not, write to the Free Software

Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 021111307 USA.

.SH BUGS

There probably are some, but we don't know what they are yet.

.SH AUTHORS

Neil Matthew and Rick Stones

Как видите, макрос вводится с помощью точки (.) в начале строки и, как правило, дается в сокращенном виде. 1 в конце первой строки — номер раздела руководства, в который помещается команда. Поскольку команды располагаются в разделе 1, именно туда мы и помещаем наше новое приложение.

Вы сможете сгенерировать собственное интерактивное руководство, изменив приведенную страницу и изучив исходный код других страниц. Можно также посмотреть в архиве на Web-странице http://www.tldp.org/ часть Linux Documentation Project (Проект документирования Linux) "Linux Man Page mini-HowTo" ("Краткое руководство по написанию страниц интерактивного руководства в Linux"), написанную Дженс Швейкхардт (Jens Schweikhardt).

Имея исходный текст страницы справочного руководства, можно обработать его утилитой groff. Команда groff обычно формирует текст ASCII (-Tascii) или выходной файл PostScript (-Tps). С помощью опции -man сообщите groff, что это страница интерактивного справочного руководства, и будут загружены специальные макроопределения, относящиеся к страницам интерактивного руководства.

$ groff -Tascii -man myapp.1

У этой команды следующий вывод.

MYAPP(1)                                                                 MYAPP(1)

NAME

       Myapp — A simple demonstration application that does very

       little.

SYNOPSIS

       myapp [-option ...]

DESCRIPTION

       myapp is a complete application that does nothing useful.

       It was written for demonstration purposes.

OPTIONS

       It doesn't have any, but let's pretend, to make this temp-

       late complete:

       -option

              If there was an option, it would not be -option.

RESOURCES

      myapp uses almost no resources.

DIAGNOSTICS

       The program shouldn't output anything, so if you find it

       doing so there's probably something wrong. The return

       value is zero.

SEE ALSO

       The only other program we know with this little func-

       tionality is the ubiquitous Hello World application.

COPYRIGHT

      myapp is Copyright (c) 2007 Wiley Publishing, Inc.

      This program is free software; you can redistribute it

      and/or modify it under the terms of the GNU General Public

      License as published by the Free Software Foundation;

      either version 2 of the License, or (at your option) any

      later version.

      This program is distributed in the hope that it will be

      useful, but WITHOUT ANY WARRANTY; without even the implied

      warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR

      PURPOSE. See the GNU General Public License for more

      details.

1

MYAPP(1)                                                           MYAPP(1)

       You should have received a copy of the GNU General Public

       License along with this program; if not, write to the Free

       Software Foundation, Inc., 59 Temple Place — Suite 330

       Boston, MA 02111-1307, USA

BUGS

       There probably are some, but we don't know what they are yet.

AUTHORS

       Neil Matthew and Rick Stones

Теперь, когда интерактивное руководство протестировано, необходимо указать для него исходный файл. Команда man, показывающая страницы руководства, использует переменную окружения MANPATH для поиска нужных страниц. Вы можете поместить новую страницу в каталог локальных страниц интерактивного руководства или прямо в системный каталог /usr/man/man1.

Когда кто-нибудь в первый раз запросит эту страницу интерактивного справочного руководства, команда man автоматически отформатирует ее и отобразит. Некоторые версии man могут автоматически генерировать и сохранять заранее отформатированные (возможно, сжатые) текстовые ASCII-версии страниц интерактивного справочного руководства, чтобы ускорить в дальнейшем выполнение запросов на вывод одной и той же страницы.

Распространение программного обеспечения

Главная задача, возникающая при распространении программного обеспечения, — гарантия того, что в дистрибутив включены все файлы правильных версий. К счастью, интернет-сообщество программистов выработало ряд очень надежных методов, которые прошли долгий путь, устраняя возникавшие проблемы. К этим методам относятся следующие:

□ создание стандартными средствами, имеющимися на всех машинах с ОС Linux, единого пакета, включающего файлы всех компонентов;

□ правляемая нумерация версий пакетов;

□ соглашение по именованию файлов, требующее включения номера версии в файл пакета, чтобы пользователи могли легко увидеть, с какой версией они работают;

□ применение подкаталогов в пакете, чтобы при извлечении файлов из него они помешались в отдельный каталог, и не возникали сомнения по поводу содержимого пакета.

Эволюция этих методов была направлена на облегчение распространения программ и повышение надежности этого процесса. Легкость установки программы — это другой вопрос, поскольку она зависит от программы и системы, в которой устанавливается программа, но, по крайней мере, вы будете уверены в том, что у вас корректные файлы всех компонентов.

Программа patch

Когда программы распространяются, почти неизбежно пользователи обнаруживают в них ошибки или у автора возникает желание внести в программу усовершенствования и обновления. Если авторы распространяют программы в виде двоичных файлов, в этом случае они часто просто отправляют новые версии двоичных файлов. Иногда (всегда чаще, чем хотелось бы) производители просто выпускают новую версию программы, часто с невразумительным описанием этой новой версии и недостаточной информацией о внесенных изменениях.

С другой стороны, отличный выход — распространение вашего программного обеспечения в виде исходного программного кода, это позволит пользователям увидеть, как вы реализовали алгоритмы и как применяли функции. Кроме того, у пользователей появится возможность проконтролировать, что именно делает программа, и повторно использовать фрагменты исходного кода (при условии соблюдения лицензионного соглашения).

Однако при объеме кода ядра Linux, равного десяткам мегабайтов сжатого исходного программного кода, доставка обновленного набора исходных файлов потребует значительных ресурсов при том, что, возможно, реально в новой версии будет изменен лишь небольшой процент этого исходного кода.

К счастью, для решения этой проблемы существует утилита patch. Она была написана Ларри Уоллом (Larry Wall), также автором языка программирования Perl. Команда patch позволяет распространять только различия между двумя версиями, так что любой обладатель файла версии 1 и файла отличий версии 2 от версии 1 сможет применить команду patch для генерации на своей машине версии 2.

Если вы начинаете с файла версии 1

This is file one

line 2

line 3

there is no line 4, this is line 5

line 6

и затем создаете версию 2

This is file two

line 2

line 3

line 4

line 5

line 6

a new line 8

с помощью команды diff можно создать список отличий:

$ diff file1.c file2.с > diffs

Файл diffs содержит следующие строки:

1c1

< This is file one

--

> This is file two

4c4, 5

< there is no line 4, this is line 5

--

> line 4

> line 5

5a7

> a new line 8

На самом деле это набор команд редактора для превращения одного файла в другой. Предположим, что у вас есть файл file1.c и файл diffs. Вы можете обновить свой файл с помощью команды patch следующим образом:

$ patch file1.c diffs

Hmm... Looks like a normal diff to me...

Patching file file1.c using Plan A...

Hunk #1 succeeded at 1.

Hunk #2 succeeded at 4.

Hunk #3 succeeded at 7.

done

$

Команда patch сделала file1.c таким же, как файл file2.c.

У команды patch есть еще один фокус: возможность отказа от внесенных изменений. Предположим, что вам не понравились изменения, и вы хотите вернуться назад к file1 с. Нет ничего проще; всего лишь воспользуйтесь командой patch еще раз, добавив опцию -R (обратная корректировка).

$ patch -R file1.c diffs

Hmm... Looks like a normal diff to me...

Patching file file1.c using Plan A...

Hunk #1 succeeded at 1.

Hunk #2 succeeded at 4.

Hunk #3 succeeded at 6.

done$

Файл file1.с возвращен в свое исходное состояние.

У команды patch есть и другие опции, но лучше всего на входе команды решить, что вы хотите сделать, а затем "выполнить верное действие". Если вдруг команда patch завершается аварийно, она создает файл с расширением rej, содержащий фрагменты, которые невозможно было исправить.

Когда вы работаете с корректировками программного обеспечения, полезно применять опцию diff -с, формирующую "окружающий контекст". Она включает несколько строк перед каждым изменением и после него, так что команда patch сможет проверить контекстные соответствия перед внесением изменений. Кроме того, в этом случае легче читать исправленный файл.

Примечание

Если вы нашли и исправили ошибку в программе, легче, точнее и вежливее отправить автору исправленный файл, а не просто описание исправления.

Другие утилиты распространения

Программы Linux и исходный код обычно распространяются в виде файлов с именами, в которые включен номер версии, и расширениями tar.gz или tgz. Это сжатые программой gzip файлы TAR (tape archive, архивы лент), также называемые "tarballs" (клубки архивов tar). Если применить обычную команду tar, обрабатывать эти файлы придется в два этапа. Приведенный далее код создает сжатый программой gzip файл TAR вашего приложения:

$ tar cvf myapp-1.0.tar main.c 2.c 3.c *.h myapp.1 Makefile5

main.c

2.c

3.c

a.h

b.h

c.h

myapp.1

Makefile5

$

Теперь у вас есть файл TAR:

$ ls -l *.tar

-rw-r--r-- 1 neil users  10240 2007-07-09 11:23 myapp-1.0.tar

$

Сделать его меньше можно с помощью программы сжатия gzip:

$ gzip myapp-1.0.tar $ ls -l *.gz

-rw-r--r-- 1 neil users 1648 2007-07-09 11:23 myapp-1.0.tar.gz

$

Как видите, в результате впечатляющее уменьшение размера. Файл tar.gz можно в дальнейшем переименовать, оставив просто расширение tgz.

$ mv myapp-1.0.tar.gz myapp_v1.tgz

Практика задания имен, заканчивающихся точкой и тремя символами, — уступка программному обеспечению, работающему в ОС Windows, которое в отличие от программ для ОС Linux и UNIX сильно зависит от наличия корректного расширения файла. Для того чтобы получить свои файлы обратно, удалите сжатие и опять извлеките их из файла, полученного с помощью tar:

$ mv myapp_v1.tgz myapp-1.0.tar.gz

$ gzip -d myapp-1.0.tar.gz

$ tar xvf myapp-1.0.tar

main.с

2.c

3.c

a.h

b.h

c.h

myapp.1

Makefile5

$

С версией GNU программы tar все еще проще — вы можете создать сжатый архив за один шаг:

$ tar zcvf myapp_v1.tgz main.c 2.c 3.c *.h myapp.1 Makefile5

main.c

2.c

3.c

a.h

b.h

c.h

myapp.1

Makefile5

$

Также легко вы можете развернуть файл:

$ tar zxvf myapp_v1.tgz

main.c

2.с

3. с

a. h

b. h c.h

myapp.1

Makefile5

$

Если хотите увидеть содержимое архива, не извлекая его, следует вызвать программу tar с несколько иной опцией: tar ztvf.

В предыдущих примерах мы применяли tar, описывая только необходимые опции. Теперь дадим краткий обзор команды и нескольких самых популярных опций. Как вы видели в примерах, у команды следующая базовая синтаксическая запись:

tar [опции] [список_файлов]

Первый элемент списка файлов — выходной файл, и хотя мы работали с файлами, он может быть и устройством. Другие элементы списка в зависимости от опций команды добавляются в новый или существующий архив.

Список файлов также может включать каталоги, в этом случае по умолчанию в файл включаются все подкаталоги. Если вы извлекаете файлы, нет необходимости задавать имена, т.к. программа tar сохраняет полные пути.

В этом разделе использовалось шесть комбинаций разных опций:

□ с — создает новый архив;

□ f — определяет, что выходной файл — не устройство, а файл;

□ t — перечисляет содержимое архива без реального извлечения элементов;

□ v (verbose) — по ходу выполнения tar выводит сообщения;

□ х — извлекает файлы из архива;

□ z — пропускает архив GNU tar через программу gzip (сжимает его или убирает сжатие).

У команды tar есть еще множество опций, позволяющих улучшить управление действиями команды и создаваемыми ею архивами. Дополнительную информацию о программе tar см. на страницах интерактивного справочного руководства.

RPM-пакеты

Диспетчер RPM-пакетов или RPM появился как создатель формата упаковки в дистрибутиве Linux Red Hat (и первоначально назывался Red Hat Package Manager). С того времени формат RPM превратился в общепринятый формат упаковки в разных дистрибутивах Linux, включая. SUSE Linux. Он также был выбран как официальный формат упаковки проектом по стандартизации операционных систем на базе Linux Linux Standards Base или LSB, см. Web-сайт www.linuxbase.org.

К основным достоинствам RPM относятся следующие.

□ Этот диспетчер широко распространен. Многие дистрибутивы Linux могут, по меньшей мере, устанавливать RPM-пакеты или использовать формат RPM как собственный формат упаковки файлов. Кроме того, RPM перенесен на многие другие операционные системы.

□ Он позволяет устанавливать RPM-пакеты с помощью одной команды. Вы также можете устанавливать пакеты автоматически, т.к. формат RPM разработан для необслуживаемого применения. Удалить или обновить пакет также можно одной командой.

□ Вы работаете с одним файлом. RPM-пакет хранился в едином файле, облегчая тем самым перенос пакета из одной системы в другую.

□ RPM автоматически выполняет проверку зависимостей. RPM-система включает в себя базу данных всех пакетов, установленных вами, вместе с данными о том, что каждый пакет дает вашей системе и информацией о требованиях каждого пакета.

□ RPM-пакеты разработаны для формирования исполняемых файлов из исходных, позволяя вам воспроизводить сборку. Диспетчер RPM поддерживает средства ОС Linux, например, команду patch для внесения изменений в программный код в процессе компиляции.

Работа с файлами RPM-пакетов

Любой RPM-пакет хранится в файле с расширением rpm. Файлы пакетов, как правило, соблюдают соглашение об именовании, предлагающее следующую структуру имени:

name-version-release.architecture.rpm

В этой структуре name содержит групповое имя пакета, например, mysql для базы данных MySQL или make для средства компиляции и компоновки make. В элементе version указывается номер версии программного обеспечения, например, версия 5.0.41 для MySQL. Элемент release хранит номер, который определяет, какой вариант или выпуск RPM указанной версии программного обеспечения содержится в файле. Это важно, потому что RPM-пакеты собираются набором инструкций (которые будут обсуждаться в разд. "Создание RPM-файла spec" далее в этой главе). Номер выпуска позволяет отслеживать изменения в инструкциях сборки.

Элемент architecture содержит спецификатор для архитектуры компьютера, на которую рассчитана программа, например, i386 для Intel-системы. Для откомпилированных программ этот элемент очень важен, поскольку исполняемый файл, созданный для процессора SPARC, вполне вероятно, не будет работать на процессоре Intel. Архитектура может задаваться обобщенно, например sparc для процессоров SPARC, или более конкретно, например sparcv9 для v9 SPARC или athlon для процессора AMD Athlon. Пока вы не переопределите этот элемент, RPM-система не даст вам установить пакеты, предназначенные для компьютера с другой архитектурой.

Элемент architecture может также содержать специальные значения: noarch для пакетов, не относящихся к архитектуре определенного типа, таких как файлы документации, программы на языке Java, модули на языке Perl, и src для RPM-пакета с исходными файлами. RPM-пакеты с исходными файлами содержат тексты программ и инструкции по сборке для построения двоичного RPM-пакета. Большинство RPM-пакетов, предлагаемых для загрузки, для удобства заранее собраны в расчете на компьютеры с архитектурой определенного типа. Вы сможете найти тысячи программ для системы Linux в виде заранее собранных и готовых к установке RPM-пакетов. Это убережет вас от трудностей компиляции.

Кроме того, некоторые пакеты так сильно зависят от конкретных версий, что проще загрузить заранее собранный пакет, чем тестировать все его компоненты вручную. Например, пакеты для беспроводных сетей стандарта 802.11b однажды пришли собранными для конкретных уровней исправлений ядра определенных дистрибутивов Linux, один из них — пакет kernel-wlan-ng-modules-rh9.18-0.2.0-7-athlon.rpm, который включал в себя модули ядра для дистрибутива Red Hat 9.0 с ядром а2.4.20-18 в системе на базе процессора AMD Athlon.

Установка RPM-пакетов

Для установки RPM-пакета запустите команду rpm. Формат очень простой:

rpm -Uhv name-version-release.architecture.rpm

Например,

$ rpm -Uhv MySQL-server-5.0.41-0.glibc23.i386.rpm

Эта команда устанавливает (или при необходимости обновляет) пакет сервера СУРБД MySQL для системы на базе Intel x86.

Команда rpm обеспечивает большую часть взаимодействия пользователя с RPM-системой. Вы можете узнать, установлен ли пакет, с помощью следующей команды:

$ rpm -qa xinetd

xinetd-2.3.14-40

Формирование RPM-пакетов

Для создания RPM-пакета выполните команду rpmbuild. Процесс относительно прост. Вы должны сделать следующее:

1. Собрать программное обеспечение, которое хотите поместить в пакет.

2. Создать файл spec, описывающий, как собирать пакет.

3. Сформировать пакете помощью команды rpmbuild.

Поскольку создание RPM-пакета может быть очень сложным, мы будем придерживаться в этой главе простого примера, достаточного для распространения приемлемого приложения в виде исходного или двоичного файла. Более таинственные опции и поддержку пакетов, полученных с помощью файлов исправлений (patches), мы оставим любознательным читателям. Для получения дополнительной информации изучите страницу интерактивного справочного руководства, посвященную программе rpm, или справочное руководство RPM HOWTO (обычно хранящееся в каталоге /usr/share/doc). Кроме того, прочтите книгу Эрика Фостера-Джонсона (Eric Foster-Johnson) "Red Hat RPM Guide" ("Справочник по Red Hat RPM"), доступную в интерактивном режиме на Web-сайте http://docs.fedoraproject.org/drafts/rpm-guide-en/.

Последующие разделы соответствуют трем шагам, необходимым для создания пакета тривиального приложения myapp.

Сбор программного обеспечения

Первый этап в создании RPM-пакета — сбор программного обеспечения, которое вы хотите поместить в пакет. Чаще всего у вас есть исходный программный код приложения, файл сборки, например make-файл, и, возможно, страница интерактивного справочного руководства.

Самый легкий способ собрать это программное обеспечение — упаковать файлы в сжатый tar-файл. Назовите файл архива именем приложения и укажите в нем номер версии, например, myapp-1.0.tar.gz.

Вы можете откорректировать ранее созданный make-файл Makefile6, добавив новое задание на упаковку файлов в сжатый файл архива. Окончательная версия make-файла, названная просто Makefile, выглядит следующим образом:

all: myapp

# Какой компилятор

CC = gcc

# Где хранятся файлы include

INCLUDE = .

# Опции для разработки

CFLAGS = -g -Wall -ansi

# Опции для рабочей версии

# CFLAGS = -О -Wall -ansi

# Локальные библиотеки

MYLIB = mylib.a

myapp: main.о $(MYLIB)

 $(CC) -о myapp main.о $(MYLIB)

$(MYLIB) : $(MYLIB)(2.o) $(MYLIB)(3.о)

main.о: main.c a.h

2.o: 2.с a.h b.h

3.o: 3.c b.h c.h

clean:

 -rm main.о 2.о 3.o $(MYLIB)

dist: myapp-1.0.tar.gz

myapp-1.0.tar.gz: myapp myapp.1

 -rm -rf myapp-1.0

 mkdir myapp-1.0

 cp *.c *.h *.1 Makefile myapp-1.0

 tar zcvf $@ myapp-1.0

Задание myapp-1.0.tar.gz в make-файле формирует сжатый архив (tarball) из исходных файлов нашего простого примера приложения. Этот код вставлен для простоты в задание dist, в котором вызываются те же команды. Для создания файла архива выполните следующую команду:

$ make dist

Далее нужно скопировать файл myapp-1.0.tar.gz в каталог RPM-пакетов SOURCES, обычно в системе Red Hat Linux это каталог /usr/src/redhat/SOURCES, а в системе SUSE Linux — /usr/src/packages/SOURCES. Например:

$ cp myapp-1.0.tar.gz /usr/src/redhat/SOURCES

RPM-система полагает, что исходные файлы находятся в каталоге SOURCES в виде tar-файлов. (Есть и другие опции, но эта самая простая.) SOURCES — это один из каталогов, на которые рассчитывает RPM-система.

RPM-система полагается на пять каталогов, приведенных в табл. 9.4.

Таблица 9.4

RPM-каталог Описание
BUILD Команда rpmbuild создает программное обеспечение в этом каталоге
RPMS Команда rpmbuild хранит в этом каталоге созданные ею двоичные файлы
SOURCES В этот каталог следует поместить исходные файлы для вашего приложения
SPECS В этот каталог следует помещать файлы spec для всех RPM-пакетов, которые вы планируете создать, хотя это и не обязательно
SRPMS Команда rpmbuild помещает в этот каталог RPM-пакеты из исходных файлов

У каталога RPMS обычно есть ряд подкаталогов, определяющих тип архитектуры системы, например такие, как приведенные далее (для системы с архитектурой Intel х86).

$ ls RPMS

athlon

i386

i486

i586

i686

noarch

В системах Red Hat Linux по умолчанию предполагается, что RPM-пакеты создаются в каталоге /usr/src/redhat.

Примечание

Этот каталог специфичен для системы Red Hat Linux. В других дистрибутивах Linux используются иные каталоги, например каталог /usr/src/packages.

После того как исходные файлы для вашего RPM-пакета будут собраны вместе, нужно создать файл spec, описывающий, как именно команда rpmbuild должна создать ваш пакет.

Создание RPM-файла spec

Создание файла spec может оказаться непростым занятием при наличии тысяч опций, поддерживаемых RPM-системой. Можно воспользоваться простым примером из этого раздела, которого будет достаточно для большинства создаваемых вами пакетов. Кроме того, можно скопировать команды из других файлов spec.

Примечание

Хорошими источниками примеров файлов spec служат другие RPM-пакеты. Посмотрите RPM-пакеты исходных файлов, хранящиеся в файлах с окончанием .src.rpm. Установите эти RPM-пакеты и просмотрите их файлы spec. Вы найдете гораздо более сложные примеры, чем те, которые вам когда-либо понадобятся. Интересные примеры можно найти среди файлов spec, предназначенных для пакетов anonftp, telnet, vnc и sendmail.

Кроме того, разработчики RPM-системы мудро решили не пытаться заменить популярные средства построения программ, такие как make или configure. RPM-система содержит много средств быстрого доступа, позволяющих воспользоваться make-файлами и сценариями configure.

В данном примере вы создаете файл spec для простого приложения myapp. Назовите его myapp.spec. Начинает файл spec с набора определения имени, номера версии и другой информации о вашем пакете. Например,

Vendor:       Wrox Press

Distribution: Any

Name:         myapp

Version:      1.0

Release:      1

Packager:     neil@provider.com

License:      Copyright 2007 Wiley Publishing, Inc

Group:        Applications/Media

Эту секция RPM-файла spec часто называют заголовком. В ней содержатся наиболее важные параметры Name, Version и Release. В нашем примере имя — myapp, номер версии — 1.0 и номер выпуска или сборки RPM-пакета — 1, т.к. эта ваша первая попытка создания RPM-пакета.

Параметр Group применяется для облегчения графической инсталляции программ, сортируя тысячи приложений для системы Linux по типам. Элемент Distribution важен, если вы создаете пакет для одного дистрибутива Linux, например, Red Hat или SUSE Linux.

Неплохо добавить в ваш файл spec комментарии. Как и сценарии командной оболочки, и make-файлы, команда rpmbuild считает комментарием любую строку, начинающуюся с символа #. Например:

# Это строка комментария.

Для того чтобы помочь пользователям решить, нужно ли им устанавливать ваш пакет, предоставьте секции Summary и %description (обратите внимание на несогласованность RPM-синтаксиса, применяющего знак процента перед обозначением секции описания). Например, свой пакет вы можете описать следующим образом:

Summary:    Trivial application

%description

MyApp Trivial Application

A trivial application used to demonstrate development tools.

This version pretends it requires MySQL at or above 3.23.

Authors: Neil Matthew and Richard Stones

Секция %description может состоять (и обычно состоит) из нескольких строк.

Файл spec может содержать сопутствующую информацию и о том, какие возможности предоставляет ваш пакет, и о том, от чего он зависит. (Вы также можете определить, от чего зависит пакет исходных файлов, например, указать специальные заголовочные файлы, необходимые для компиляции.)

Параметр Provides определяет возможности, предоставляемые вашей системой. Например:

Provides: goodness

В примере утверждается, что пакет предоставляет вымышленную функциональную возможность, именуемую goodness (ценные свойства). RPM-система также автоматически добавляет элемент Provides к имени пакета, в данном случае myapp. Параметры Provides полезны в случае множественных пакетов, предоставляющих одну и ту же функциональную возможность. Например, пакет Web-сервера Apache предоставляет средство webserver. Другие пакеты, например Thy, могут предоставлять то же средство. (Для облегчения работы с конфликтующими пакетами RPM-система позволяет задавать также информацию с помощью элементов Conflicts и Obsoletes.)

Наиболее важная сопутствующая информация определяется в параметрах Requires. Вы можете указать все пакеты, необходимые для функционирования вашего пакета. Например, Web-серверу требуется сетевой пакет и пакет безопасности. В нашем примере вы задаете необходимость СУРБД MySQL версии 3.23 или более свежей. Синтаксическая запись приведена далее:

Requires: mysql >= 3.23

Если вам нужна СУРБД MySQL любой версии, можно задать параметр следующим образом:

Requires: mysql

RPM-система не разрешит пользователям устанавливать пакеты, если не установлены пакеты, необходимые для их работы. (Правда, пользователи могут переопределить это поведение.)

RPM-система автоматически добавляет зависимые элементы, например /bin/sh для сценариев командной оболочки, интерпретатор Perl для сценариев на языке Perl и любые совместно используемые библиотеки (файлы с расширением so), которые вызывает ваше приложение. Каждая новая версия RPM-системы включает все новые средства для автоматической проверки зависимостей.

После задания требований необходимо определить исходные файлы, формирующие ваше приложение. Для большинства приложений можно просто скопировать следующую строку:

source: %{name}-%{version}.tar.gz

Синтаксическая запись %{name} ссылается на RPM-макрос, в данном случае имя пакета. Поскольку ранее вы задали имя myapp, команда rpmbuild заменит %{name} на myapp и аналогично заменит %{version} на 1.0, для того чтобы использовать для построения файл с именем myapp-1.0.tar.gz. Искать этот файл она будет в каталоге SOURCES, описанном ранее.

В примере задается параметр Buildroot, определяющий место установки пакета. Вы можете скопировать в ваши пакеты следующую строку:

Buildroot: %{_tmppath}/%{name}-%{version}-root

После того как параметр Buildroot задан, устанавливайте ваши приложения в каталог из параметра Buildroot. Можно использовать удобную переменную $RPM_BUILD_ROOT, которая задается для всех сценариев командной оболочки в файле spec.

После задания всех этих характеристик пакета далее нужно определить, как собирать пакет. Для этого есть четыре основные секции: %prep, %build, %install и %clean.

Судя по имени, секция %prep предназначена для подготовки сборки. В большинстве случаев вы можете выполнить приведенный далее макрос %setup с параметром -q для перевода его в режим без вывода сообщений:

%prep

%setup -q

Секция %build собирает ваше приложение. В большинстве случаев можно применять простую команду make. Например:

%build

make

Это один из способов, которым RPM-система использует уже проделанную вами работу по созданию make-файла.

Секция %install устанавливает ваше приложение, интерактивное справочное руководство и любые файлы поддержки. Вы можете применить RPM-макрос %makeinstall, который вызывает задание install make-файла. Тем не менее, в данном случае установим файлы вручную, чтобы продемонстрировать дополнительные RPM-макросы:

%install

mkdir -р $RPM_BUILD_ROOT%{_bindir}

mkdir -p $RPM_BUILD_ROOT%{_mandir}

install -m755 myapp $RPM_BUILD_ROOT%{_bindir}/myapp

install -m755 myapp.1 $RPM_BUILD_ROOT%{_mandir}/myapp.1

В этом примере при необходимости создаются каталоги для файлов, а затем устанавливаются исполняемый файл myapp и интерактивное справочное руководство myapp.1. Переменная окружения $RPM_BUILD_ROOT содержит местоположение Buildroot, заданное ранее. Макросы %{_bindir} и %{_mandir} замещаются текущим каталогом двоичных файлов и каталогом страниц интерактивного справочного руководства соответственно.

Примечание

Если вы пользуетесь сценарием configure для создания make-файла, все разнообразные каталоги в нем будут заданы должным образом. В большинстве случаев вам не придется задать все команды установки вручную, как. показано в предыдущем примере.

Задание %clean удаляет файлы, созданные командой rpmbuild. Например:

%clean

rm -rf $RPM_BUILD_ROOT

После описания построения пакета следует задать все файлы, которые будут устанавливаться. RPM-система очень строга на этот счет. Она и должна быть строгой для того, чтобы иметь возможность отследить должным образом каждый файл в каждом пакете. В секции %files перечисляются имена всех файлов, включаемых в пакет. В данном случае у нас только два файла предназначены для распространения в двоичном пакете: исполняемый файл myapp и страница интерактивного справочного руководства myapp.1. Например:

%files

%{_bindir}/myapp

%{_mandir}/myapp.1

RPM-система может выполнять сценарий до и после установки вашего пакета. Например, если ваш пакет — процесс-демон, для его запуска, возможно, нужна корректировка сценариев установки системы. Сделайте это с помощью сценария %post. Далее приведен простой пример, отправляющий сообщение по электронной почте:

%post

mail root -s "myapp installed — please register" </dev/null

Поищите примеры в серверных RPM-файлах spec.

Далее приводится полный файл spec для вашего простого приложения.

#

# spec file for package myapp (Version 1.0)

#

Vendor:       Wrox Press

Distribution: Any

Name:         myapp

Version:      1.0

Release:      1

Packager:     neil@provider.com

License:      Copyright 2007 Wiley Publishing, Inc.

Group:        Applications/Media

Provides:     goodness

Requires:     mysql >=3.23

Buildroot:    %{_tmppath}/%{name}-%{version}-root

source:       %{name}-%{version}.tar.gz

Summary:      Trivial application

%description

MyApp Trivial Application

A trivial application used to demonstrate development tools.

This version pretends it requires MySQL at or above 3.23.

Authors: Neil Matthew and Richard Stones

%prep

%setup -q

%build

make

%install

mkdir -p $RPM_BUILD_ROOT%{bindir}

mkdir -p $RPM_BUILD_ROOT%{_mandir}

install -m755 myapp $RPM_BUILD_ROOT%{_bindir}/myapp

install -m755 myapp.1 $RPM_BUILD_ROOT%{_mandir}/myapp.1

%clean

rm -rf $RPM_BUILD_ROOT

%post

mail root -s "myapp installed — please register" </dev/null

%files

%{_bindir}/myapp

%{_mandir}/myapp.1

Теперь вы готовы к формированию RPM-пакета.

Создание RPM-пакета с помощью rpmbuild

Создаются пакеты с помощью команды rpmbuild со следующей синтаксической записью:

rpmbuild -bBuildStage spec_file

Опция -b заставляет rpmbuild создать RPM-пакет. Дополнительная опция BuildStage — специальный код, информирующий команду rpmbuild о том, как далеко она может зайти в процессе создания. В табл. 9.5 перечислены опции команды.

Таблица 9.5

Опция Описание
-ba Создавать и двоичный, и исходный RPM-пакет
-bb Создавать двоичный RPM-пакет
-bc Компилировать программу, но не создавать полный RPM-пакет
-bp Подготовиться к созданию двоичного RPM-пакета
-bi Создать двоичный RPM-пакет и установить его
-bl Проверить список файлов RPM-пакета
-bs Создать только RPM-пакет исходных файлов

Для создания двоичного RPM-пакета и пакета исходных файлов используйте опцию -ba. RPM-пакет исходных файлов позволит создать повторно двоичный RPM- пакет.

Скопируйте RPM-файл spec в корректный каталог SOURCES, поместив его рядом с исходным файлом приложения:

$ cp myapp.spec /usr/src/redhat/SOURCES

Далее приведен вывод, сопровождающий создание пакета в системе SUSE Linux, пакеты в которой создаются из каталога /usr/src/packages/SOURCES:

$ rpmbuild -ba myapp.spec

Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.47290

+ umask 022

+ cd /usr/src/packages/BUILD

+ cd /usr/src/packages/BUILD

+ rm -rf myapp-1.0

+ /usr/bin/gzip -dc /usr/src/packages/SOURCES/myapp-1.0.tar.gz

+ tar -xf -

+ STATUS=0

+ '[' 0 -ne 0 '] '

+ cd myapp-1.0

++ /usr/bin/id -u

+ '[' 1000 = 0 ']'

++ /usr/bin/id -u

+ '[' 1000 = 0 ']'

+ /bin/chmod -Rf a+rX, u+w, g-w, o-w

+ exit 0

Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.99663

+ umask 022

+ cd /usr/src/packages/BUILD

+ /bin/rm -rf /var/tmp/myapp-1.0-root

++ dirname /var/tmp/myapp-1.0-root

+ /bin/mkdir -p /var/tmp

+ /bin/mkdir /var/tmp/myapp-1.0-root

+ cd myapp-1.0 + make

gcc -g -Wall -ansi -с -o main.о main.c

gcc -g -Wall -ansi -с -o 2.o 2.c

ar rv mylib.a 2.o

ar: creating mylib.a

a - 2.о

gcc -g -Wall -ansi -с -o 3.o 3.c

ar rv mylib.a 3.o

a — 3.o

gcc -o myapp main.о mylib.a

+ exit 0

Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.47320

+ umask 022

+ cd /usr/src/packages/BUILD

+ cd myapp-1.0

+ mkdir -p /var/tmp/myapp-1.0-root/usr/bin

+ mkdir -p /var/tmp/myapp-1.0-root/usr/share/man

+ install -m755 myapp /var/tmp/myapp-1.0-root/usr/bin/myapp

+ install -m755 myapp.1 /var/tmp/myapp-1.0-root/usr/share/man/myapp.1

+ RPM_BUILD_ROOT=/var/tmp/myapp-1.0-root

+ export RPM_BUILD_ROOT

+ test -x /usr/sbin/Check -a 1000 = 0 -o

 -x /usr/sbin/Check -a '!' -z /var/tmp/myapp-1.0-root

+ echo 'I call /usr/sbin/Check...'

I call /usr/sbin/Check...

+ /usr/sbin/Check

-rwxr-xr-x 1 neil users 926 2007-07-09 13:35 /var/tmp/myapp-1.0-root/ /usr/share/man/myapp.1.gz

Checking permissions and ownerships — using the permissions files

 /tmp/Check.perms.017506

setting /var/tmp/myapp-1.0-root/ to root:root 0755 (wrong owner/group neil:users)

setting /var/tmp/myapp-1.0-root/usr to root:root 0755. (wrong owner/group neil:users)

+ /usr/lib/rpm/brp-compress

+ /usr/lib/rpm/brp-symlink

Processing files: myapp-1.0-1

Finding Provides: /usr/lib/rpm/find-provides myapp

Finding Requires: /usr/lib/rpm/find-requires myapp

Finding Supplements: /usr/lib/rpm/find-supplements myapp

Provides: goodness

Requires(interp): /bin/sh

Requires(rpmlib): rpmlib(PayloadFilesHavePrefix) <= 4.0-1

 rpmlib (CompressedFileNames) <= 3.0.4-1

Requires(post): /bin/sh

Requires: mysql >= 3.23 libc.so.6 libc.so.6 (GLIBC 2.0)

Checking for unpackaged file(s): /usr/lib/rpm/check-files /var/tmp/myapp-1.0-root

Wrote: /usr/src/packages/SRPMS/myapp-1.0-1.src.rpm

Wrote: /usr/src/packages/RPMS/i586/myapp-1.0-1.i586.rpm

Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.10065

+ umask 022

+ cd /usr/src/packages/BUILD

+ cd myapp-1.0

+ rm -rf /var/tmp/myapp-1.0-root

+ exit 0

Когда сборка будет закончена, вы должны увидеть два пакета: двоичный RPM-пакет в подкаталоге с названием типа архитектуры, например i586 каталога RPMS, и RPM-пакет исходных файлов в каталоге SRPMS.

У файла двоичного RPM-пакета будет имя, похожее на следующее:

myapp-1.0-1.i586.rpm

У вашей системы может быть другая архитектура.

Имя файла RPM-пакета исходных файлов будет следующим:

myapp-1.0-1.src.rpm

Примечание

Пакеты должен устанавливать суперпользователь. Создавать пакеты от имени пользователя root нет необходимости, если у вас есть права на запись в каталоги RPM-системы, обычно это каталоги /usr/src/redhat. Как правило, не следует создавать RPM-пакеты как пользователь root, потому что в файле spec могут быть команды, способные повредить вашу систему.

Пакеты других форматов

Несмотря на то, что RPM — популярный способ распространения приложений, позволяющий пользователям управлять установкой и деинсталляцией пакетов, существуют и конкурирующие пакеты. Некоторое программное обеспечение все еще распространяется в виде сжатых программой gzip tar-файлов (tgz). Обычно инсталляция состоит из распаковки архива во временный каталог и затем выполнения сценария непосредственно установки.

Дистрибутивы Linux Debian и на основе Debian (а также некоторые другие) поддерживают другой формат упаковки, по функциональности похожий на RPM и именуемый dpkg. Утилита dpkg дистрибутива Debian распаковывает и устанавливает файлы пакета, обычно имеющие расширение deb. Если вам нужно распространять приложение как файл пакета с расширением deb, можно преобразовать RPM-пакет в формат dpkg с помощью утилиты Alien. Дополнительную информацию о ней можно найти на Web-сайте http://kitenet.net/programs/alien/.

Среды разработки

Почти все средства, рассматриваемые до сих пор в этой главе, по существу представляют собой средства режима командной строки. У разработчиков, работавших в ОС Windows, несомненно есть опыт работы с интегрированными средами разработки (IDE, Integrated Development Environment). IDE — это графическая оболочка, в которой собраны вместе все или некоторые средства, необходимые для создания, отладки и выполнения приложения. Обычно она как минимум содержит редактор, обозреватель файлов и средство для выполнения приложения и перехвата результата. В более полные среды включена поддержка генерации исходных файлов на базе шаблонов, разработанных для приложений определенных типов, интеграция с системой управления исходным программным кодом и автоматическое документирование.

В следующих разделах мы рассмотрим одну такую IDE, KDevelop, и упомянем другие IDE, доступные для ОС Linux сегодня. Эти среды разработки активно развиваются, и лучшие из них начинают конкурировать с коммерческими предложениями.

KDevelop

KDevelop — это IDE для программ на языках С и С++. Она обеспечивает особую поддержку при создании приложений, выполняющихся в среде K Desktop Environment (KDE), одном из двух основных современных пользовательских графических интерфейсов в системах Linux. Ее можно использовать и для проектов других типов, включая простые программы на языке С.

KDevelop — бесплатное программное обеспечение, выпускаемое в соответствии с требованиями Общедоступной лицензии проекта GNU (General Public License, GPL), и имеющееся во многих дистрибутивах Linux. Самую свежую версию можно загрузить с Web-сайта http://www.kdevelop.org. Проекты, созданные с помощью среды KDevelop, по умолчанию следуют стандартам, принятым для проектов GNU. Например, они будут применять утилиту autoconf для генерации make-файлов, которые специально приспособлены к среде, для которой формируются. Это означает, что проект готов к распространению в виде исходного кода, который с большой вероятностью будет успешно откомпилирован в других системах.

Проекты KDevelop также содержат шаблоны для создания документации, текст лицензии GPL и общие инструкции по установке. Количество файлов, генерируемых при создании проекта KDevelop, может испугать, но познакомьтесь с кем-нибудь, кто загружал из Интернета и компилировал типовое приложение GPL.

Рис. 9.2 

В среде KDevelop существует поддержка систем CVS и Subversion для управления исходным программным кодом, и приложения могут редактироваться и отлаживаться без выхода из среды разработки. На рис. 9.2 и 9.3 показано стандартное приложение на С в среде KDevelop (еще одна программа, приветствующая мир), которое редактируется и выполняется.

Рис. 9.3 

Другие среды разработки

Для ОС Linux имеется в наличии иди разрабатывается множество других редакторов и IDE, как бесплатных, так и коммерческих. Несколько самых интересных приведено в табл. 9.6.

Таблица 9.6

Среда разработки Тип URL программного продукта
Eclipse Платформа на базе языка Java и IDE http://www.eclipse.org
Anjuta IDE для пользовательского графического интерфейса GNOME http://anjuta.sourceforge.net/
QtEZ IDE для пользовательского графического интерфейса KDE http://projects.uid0.sk/qtez/
SlickEdit Коммерческий редактор кода с поддержкой многих языков http://www.slickedit.com/

Резюме

В этой главе вы увидели лишь несколько средств ОС Linux, делающих разработку и распространение программ управляемыми. Первое и, может быть, самое важное — вы применили команду make и make-файлы для управления множественными исходными файлами. Далее вы познакомились с управлением исходным программным кодом с помощью систем RCS и CVS, которые позволяют отслеживать изменения в процессе разработки программ. Затем вы рассмотрели распространение программ с помощью команды patch, совместного применения команд tar и gzip и RPM-пакетов. В заключение вы бросили взгляд на одно из средств, IDE KDevelop, немного облегчающее цикл разработки программы, включающий редактирование, выполнение и отладку. 

Глава 10

Отладка

По утверждению Software Engineering Institute (Институт программных разработок) и IEEE (Institute of Electrical and Electronics Engineers, Институт инженеров по электротехнике и электронике) в любом значимом фрагменте программного обеспечения первоначально всегда есть дефекты, примерно два на 100 строк программного кода. Эти ошибки приводят к тому, что программы и библиотеки не работают так, как требуется, часто заставляя программу вести себя иначе, чем предполагалось. Отслеживание ошибок, их идентификация и удаление могут потребовать от программиста больших затрат времени на этапе разработки.

В этой главе мы рассмотрим недочеты программного обеспечения и некоторые средства и методы исследования характерных примеров ошибочного поведения. Это не то же самое, что тестирование (задача проверки работы программы во всех возможных условиях или обстоятельствах), хотя тестирование и отладка конечно же взаимосвязаны, и многие ошибки обнаруживаются в процессе тестирования.

Будут обсуждаться следующие темы:

□ типы ошибок;

□ общие методы отладки;

□ отладка с помощью gdb и других средств;

□ проверка соблюдения условий (макрос assert);

□ устранение ошибок использования памяти.

Типы ошибок

Ошибка, как правило, возникает по одной из нескольких причин, каждая из которых предполагает конкретный метод выявления и устранения.

□ Ошибки описания или спецификации. Если программа неверно определена, она, несомненно, не сможет выполняться, как требуется. Даже лучший программист в мире может порой написать неверную программу. Прежде чем приступить к программированию (или разработке), убедитесь в том, что вы точно знаете и четко представляете, что должна делать программа. Вы обнаружите и устраните множество ошибок спецификации (если не все), обсуждая требования и получая подтверждение их правильности у тех, кто будет применять вашу программу в дальнейшем.

□ Ошибки проектирования или разработки. Перед созданием программы любого размера должны прорабатываться. Как правило, недостаточно просто сесть к клавиатуре компьютера, непосредственно набрать программный код и ждать, что программа сразу заработает. Нужно время, чтобы подумать о том, как написать программу, какие структуры данных потребуются и как они будут использоваться. Постарайтесь заранее разработать все в деталях, это убережет вас от многочисленных переработок программы в дальнейшем.

□ Ошибки кодирования. Конечно, все делают ошибки при наборе. Создание программного кода из вашей разработки — неидеальный процесс. Именно здесь появляется много ошибок. Когда вы сталкиваетесь с ошибкой в программе, не упускайте возможности еще раз прочесть ваш исходный код или попросите об этом кого-нибудь. Просто поразительно, как много ошибок и недочетов можно обнаружить и устранить, обсуждая реализацию с кем-нибудь еще.

Примечание

Языки программирования с компиляторами, такие как С, обладают возможностью поймать синтаксические ошибки в процессе компиляции, в то время как интерпретируемые языки, например язык командной оболочки Linux, могут обнаружить синтаксические ошибки только тогда, когда вы попытаетесь выполнить программу. Если проблема в коде обработки ошибки, нелегко будет выявить ее в ходе тестирования.

 Попытайтесь выполнить основную часть программы на бумаге, этот процесс называют формальным прогоном. Для наиболее важных подпрограмм запишите значения на входе и вычислите шаг за шагом выходные значения. Для отладки совсем не обязательно всегда применять компьютер, иногда именно компьютер создает проблемы. Даже разработчики, пишущие библиотеки, компиляторы и операционные системы, делают ошибки! С другой стороны, не спешите винить во всем используемые программные средства; гораздо вероятнее, что ошибка закралась в вашу новую программу, а не в компилятор.

Общие методы отладки

Существует несколько разных подходов к отладке и тестированию типовой программы Linux. Обычно разработчик запускает программу и смотрит, что происходит. Если программа не работает, необходимо решить, что с ней делать. Можно изменить программу и попробовать снова (анализ программного кода, метод проб и ошибок), можно попытаться получить больше информации о том, что происходит внутри программы (оснащение контрольными средствами) или можно непосредственно проанализировать работу программы (контролируемое выполнение). Отладка включает в себя пять следующих этапов:

□ тестирование — поиск существующих изъянов или ошибок;

□ стабилизация — обеспечение повторяемости ошибок;

□ локализация — определение строки кода, отвечающей за ошибку;

□ корректировка — исправление программного кода;

□ проверка — подтверждение того, что исправление работает.

Программа с ошибками

Давайте рассмотрим пример программы, содержащей ошибки. Читая данную главу, вы будете пробовать отладить эту программу. Она написана во время разработки большой программной системы. Ее задача — протестировать единственную функцию sort, которая предназначена для реализации сортировки массива структур типа item методом "пузырька". Элементы сортируются по возрастанию поля key. Программа вызывает функцию sort для сортировки контрольного примера, чтобы протестировать функцию. В реальной жизни вы никогда не стали бы обращаться к этому конкретному алгоритму из-за его очень низкой эффективности. Мы же применяем его, потому что он короткий, относительно простой и его легко превратить в неправильный. На самом деле в стандартной библиотеке языка С есть функция с именем qsort, выполняющая эту задачу.

К сожалению, исходный код программы нелегко читается, в нем нет комментариев, и автор уже недоступен. Вам придется биться с ней самостоятельно, начиная с основной подпрограммы debug1.c.

/*  1 */ typedef struct {

/*  2 */  char *data;

/*  3 */  int key;

/*  4 */ } item;

/*  5 */

/*  6 */ item array[] = {

/*  7 */  {"bill", 3},

/*  8 */  {"neil", 4},

/*  9 */  {"john", 2},

/* 10 */  {"rick", 5},

/* 11 */  {"alex", 1},

/* 12 */ };

/* 13 */

/* 14 */ sort(a, n)

/* 15 */ item *a;

/* 16 */ {

/* 17 */  int i = 0, j = 0;

/* 18 */  int s = 1;

/* 19 */

/* 20 */  for(; i < n && s != 0; i++) {

/* 21 */   s = 0;

/* 22 */   for(j = 0; j < n; j++) {

/* 23 */    if(a[j].key > a[j + 1].key) {

/* 24 */     item t = a[j];

/* 25 */     a[j] = a[j+1];

/* 26 */     a[j+1] = t;

/* 27 */     s++;

/* 28 */    }

/* 29 */   }

/* 30 */   n--;

/* 31 */  }

/* 32 */ }

/* 33 */

/* 34 */ main()

/* 35 */ {

/* 36 */  sort(array,5);

/* 37 */ }

Теперь попытайтесь откомпилировать эту программу:

$ сс -о debug1 debug1.с

Она компилируется успешно без каких-либо сообщений об ошибках или предупреждений.

Прежде чем выполнять эту программу, вставьте фрагмент кода для вывода результата. В противном случае вы не будете знать, отработала ли программа. Вы добавите несколько дополнительных строк для отображения массива после сортировки. Назовите новую версию debug2.c.

/* 33 */ #include <stdio.h>

/* 34 */ main()

/* 35 */ {

/* 36 */  int i;

/* 37 */  sort(array, 5);

/* 38 */  for(i = 0; i < 5; i++)

/* 39 */   printf("array[3d] = (%s, %d)\n",

/* 40 */    i, array[i].data, array[i].key);

/* 41 */ }

Этот дополнительный код, строго говоря, не является частью, позже добавленной программистом. Мы заставили вас добавить его только для тестирования программы. Следует быть очень внимательным, чтобы не внести новых ошибок в ваш тестовый код. Теперь снова откомпилируйте программу и на этот раз выполните ее:

$ cc -о debug2 debug2.с

$ ./debug2

Что произойдет, когда вы сделаете это, зависит от вашей версии Linux (или UNIX) и особенностей ее установки. В своих системах мы получили следующий результат:

array[0] = {john, 2}

array[1] = {alex, 1}

array[2] = {(null), -1}

array[3] = {bill, 3}

array[4] = {neil, 4}

В еще одной системе (запускающей другое ядро Linux) мы получили следующий вывод:

Segmentation fault

В вашей системе Linux вы увидите один из приведенных результатов или совсем другой. Мы рассчитывали получить приведенный далее вывод:

array[0] = {alex, 1}

array[1] = {john, 2}

array[2] = {bill, 3}

array[3] = {neil, 4}

array[4] = {rick, 5}

Ясно, что в данном программном коде есть серьезная ошибка. Он не выполняет сортировку корректно, если вообще работает, а если он завершается с ошибкой сегментации, то операционная система посылает сигнал программе, сообщая о том, что обнаружен несанкционированный доступ к памяти, и преждевременно завершает программу, чтобы не испортить данные в оперативной памяти.

Способность операционной системы обнаружить несанкционированный доступ к памяти зависит от настройки оборудования и некоторых тонкостей реализации системы управления памятью. В большинстве систем объем памяти, выделяемый программе операционной системой, больше реально используемого. Если несанкционированный доступ осуществляется к этому участку памяти, оборудование может не выявить несанкционированный доступ. Вот почему не все версии Linux и UNIX сгенерируют сигнал о нарушении сегментации.

Примечание

Некоторые библиотечные функции, такие как printf, в определенных обстоятельствах также будут препятствовать некорректному доступу, например при использовании указателя null.

Когда вы исследуете проблемы доступа к элементам массива, часто полезно увеличить размер этих элементов, поскольку это увеличит размер ошибки. Если вы читаете единственный байт за пределами массива байтов, это может вам сойти с рук, т.к. память, выделенная программе, будет округляться до величины, зависящей от операционной системы, возможно, равной 8 Кбайт.

Если вы увеличите размер элемента массива, заменив элемент типа item массивом из 4096 символов, любое обращение к несуществующему элементу массива, возможно, окажется за пределами выделенной памяти. Каждый элемент массива равен 4 Кбайт, поэтому некорректно используемый участок памяти будет находиться за концом массива на расстоянии от 0 до 4 Кбайт.

Если мы внесем эту поправку, назвав результат debug3.c, то получим ошибку сегментации в версиях Linux обоих авторов.

/* 2 */ char data[4096];

$ сс -о debug3 debug3.с

$ ./debug3

Segmentation fault

Возможно, что какие-то варианты систем Linux или UNIX все еще не будут выдавать сообщение об ошибке сегментации. Когда стандарт ANSI С утверждает, что поведение не определено, на самом деле он разрешает программе делать все, что угодно. Это выглядит так, как будто мы написали не удовлетворяющую стандартам программу на языке С, и она может демонстрировать очень странное поведение! Как видите, изъян в программе переводит ее в категорию программ с непредсказуемым поведением.

Анализ кода

Как мы упоминали ранее, часто, если программа не работает, как ожидалось, неплохо перечитать ее. Предположим, что мы просмотрели программный код примера этой главы и исправили в нем все очевидные ошибки.

Примечание

Анализ кода — это термин, применяемый для обозначения более формального процесса, в ходе которого группа разработчиков тщательно просматривает несколько сотен строк программного кода, но масштаб не имеет значения, это все равно анализ кода, и он остается очень полезным методом поиска ошибок.

Существуют средства, которые могут помочь в анализе кода, одно из самых очевидных — компилятор. Он сообщит вам о любых имеющихся в вашей программе синтаксических ошибках.

Примечание

У некоторых компиляторов есть опции, формирующие предупреждения в сомнительных случаях, таких как отсутствие инициализации переменных или применение присваиваний в условиях. Например, компилятор GNU можно запускать со следующими опциями:

gcc -Wall -pedantic -ansi

Они порождают много предупреждений и дополнительных проверок на соответствие стандартам языка С. Рекомендуем взять за правило использование этих опций, особенно Wall. Она генерирует полезную информацию при обнаружении ошибок в программе.

Чуть позже мы кратко обсудим и другие средства, lint и splint. Как и компилятор, они анализируют код и сообщают о фрагментах кода, которые могут быть некорректными.

Оснащение средствами контроля

Оснащение средствами контроля — это вставка в программу кода для сбора дополнительной информации о поведении программы во время ее выполнения. Очень популярна вставка вызовов функции printf для вывода значений переменных на разных стадиях выполнения программы. Вы можете с пользой для себя добавить несколько вызовов printf, но должны знать о том, что этот процесс повлечет за собой дополнительные редактирование и компиляцию при любом изменении программы и, конечно, вам придется удалить код, когда ошибки будут исправлены.

Здесь могут помочь два метода оснащения средствами контроля. Первый использует препроцессор языка С для выборочного включения кода средств контроля так, что вам нужно только перекомпилировать программу для вставки или удаления отладочного кода. Сделать это можно очень просто, с помощью конструкций, подобных приведенным далее:

#ifdef DEBUG

 printf("variable x has value = %d\n", x);

#endif

Вы можете компилировать программу с флагом компилятора -DDEBUG для определения символического имени DEBUG и включения дополнительного кода и без этого флага — для удаления отладочного кода. Можно создать и более сложный вариант использования пронумерованных отладочных макросов:

#define BASIC_DEBUG 1

#define EXTRA_DEBUG 2

#define SUPER_DEBUG 4

#if (DEBUG & EXTRA_DEBUG)

 printf...

#endif

В этом случае вы всегда должны определять макрос DEBUG, но можете настраивать объем отладочной информации или уровень детализации. Флаг компилятора -DDEBUG=5 в нашем примере активизирует макросы BASIC_DEBUG и SUPER_DEBUG, но не EXTRA_DEBUG. Флаг DDEBUG=0 отключит всю отладочную информацию. С другой стороны, вставка следующих строк устранит необходимость задания в командной строке DEBUG, если отладки не требуется.

#ifndef DEBUG

#define DEBUG 0

#endif

Несколько макросов, определенных препроцессором С, могут предоставить отладочную информацию. Эти макросы раскрываются для предоставления сведений о текущей компиляции (табл. 10.1).

Обратите внимание на то, что приведенные символические имена начинаются и заканчиваются двумя символами подчеркивания. Это стандартное правило для символических имен препроцессора, и вы должны аккуратно выбирать идентификаторы, чтобы избежать конфликтов. Термин "текущие" в предыдущих описаниях указывает на момент выполнения препроцессорной обработки, т.е. время и дата запуска компилятора и обработки файла.

Таблица 10.1

Макрос Описание
__LINE__ Десятичная константа, предоставляющая номер текущей строки
__FILE__ Строка, предоставляющая имя текущего файла
__DATE__ Строка в форме "ммм дд гггг", текущая дата
__TIME__ Строка в форме "чч:мм:сс", текущее время

Выполните упражнение 10.1.

Упражнение 10.1. Отладочная информация

Далее приведена программа cinfo.c, которая выводит дату и время компиляции, если включен режим отладки.

#include <stdio.h>

# include <stdlib.h>

int main() {

#ifdef DEBUG

 printf("Compiled: " __DATE__ " at " __TIME__ "\n");

 printf("This is line %d of file %s\n", __LINE__, __FILE__);

#endif

 printf("hello world\n");

 exit(0);

}

Когда вы откомпилируете эту программу с включенным режимом отладки (используя флаг -DDEBUG), то увидите следующие сведения о компиляции:

$ cc -о cinfo -DDEBUG cinfo.c

$ ./cinfo

Compiled: Jun 30 2007 at 22:58:43

This is line 8 of file cinfo.c

hello world

$

Как это работает

Препроцессор С, часть компилятора, отслеживает текущую строку и текущий файл во время компиляции. Он подставляет текущие (времени компиляции) значения этих переменных везде, где обнаруживает символические имена __LINE__ и __FILE__. Дата и время компиляции становятся доступными аналогичным образом.

Поскольку __DATE__ и __TIME__ — строки, вы можете объединить их в функции printf с помощью строк формата, т.к. в языке С ANSI смежные строки воспринимаются как одна.

Отладка без перекомпиляции

Прежде чем двигаться дальше, стоит отметить, что существует способ применения функции printf, позволяющий отлаживать программу без применения метода #ifdef DEBUG, требующего перекомпиляции программы перед ее использованием.

Метод заключается во вставке глобальной переменной как флага отладки, разрешении опции -d в командной строке, которая дает возможность пользователю включить отладку даже после того, как программа была введена в эксплуатацию, и включении функции мониторинга процесса отладки. Теперь можно вкраплять в код программы строки, подобные следующим:

if (debug) {

 sprintf(msg, ...)

 write_debug(msg)

}

Записывать вывод отладки следует в стандартный поток ошибок stderr или, если это не годится из-за характера программы, используйте возможности мониторинга, предоставляемые функцией syslog.

Если вы вставляете в программу подобную трассировку для решения проблем, возникающих на этапе разработки, просто оставьте этот код в программе. Если вы будете чуть внимательнее, чем всегда, такой подход не вызовет никаких проблем. Выигрыш проявится, когда программа будет введена в эксплуатацию; если пользователи обнаружат проблему, они смогут выполнить программу в режиме отладки и диагностировать ошибки для вас. Вместо известия о том, что программа выдает сообщение о нарушении сегментации, они смогут написать, что конкретно делает программа в ходе выполнения, а не только описать свои действия. Разница может оказаться огромной.

У этого метода есть явный недостаток: программа становится больше, чем должна быть. В большинстве случаев это, скорее, мнимая проблема, чем реальная. Программа может стать на 20–30% больше, но чаще всего это не оказывает никакого существенного влияния на ее производительность. Снижение производительности наступает при увеличении размера на несколько порядков, а не на небольшую величину.

Контролируемое выполнение

Вернемся к примеру программы. У вас есть ошибка. Вы можете изменить программу, вставив в нее дополнительный код для вывода значений переменных по мере выполнения программы, или применить отладчик для контроля над выполнением программы и просмотра ее состояния в ходе выполнения.

В коммерческих UNIX-системах есть ряд отладчиков, набор которых зависит от поставщика системы. Наиболее распространенные — adb, sdb, idebug и dbx. Более сложные отладчики позволяют просматривать с некоторой степенью детализации состояние программы на уровне исходного кода. Именно к таким относится отладчик GNU, gdb, который может применяться в системах Linux и многих вариантах UNIX. Существуют и внешние интерфейсы (или программы-клиенты) для gdb, делающие его более удобным для пользователя; к таким программам относятся xxgdb, KDbg и ddd. Некоторые IDE, например, те, с которыми вы познакомились в главе 9, также предоставляют средства отладки или внешний интерфейс для gdb. У редактора Emacs даже есть средство (gdb-mode), позволяющее запускать gdb в вашей программе, устанавливать точки останова и построчно просматривать выполнение исходного кода.

Для подготовки программы к отладке необходимо откомпилировать ее с одной или несколькими специальными опциями. Эти опции заставляют компилятор вставлять в программу дополнительную отладочную информацию. Она включает в себя идентификаторы и номера строк — сведения, которые отладчик может использовать, чтобы показать пользователю, до какого места в исходном программном коде дошло выполнение.

Флаг -g — один из обычно применяемых при компиляции программы с последующей отладкой. Вы должны указывать его при компиляции всех исходных файлов, которые нуждаются в отладке, а также для компоновщика, чтобы могли применяться специальные версии стандартной библиотеки С, обеспечивающие поддержку режима отладки в библиотечных функциях. Программа компилятора передаст флаг компоновщику автоматически. Отладка может применяться и с библиотеками, не откомпилированными для этой цели, но с меньшей гибкостью.

Отладочная информация может увеличить исполняемый файл во много (до десяти) раз. Несмотря на увеличение размера исполняемого файла (он займет больше места на диске), объем памяти, необходимый для выполнения программы, практически остается тем же самым. Перед вводом программы в эксплуатацию неплохо удалить из нее отладочную информацию, но только после того, как программа полностью отлажена.

Примечание

Удалить отладочную информацию из исполняемого файла без повторной компиляции можно, выполнив команду strip <файл>.

Отладка с помощью gdb

Для отладки программы вы можете применять отладчик проекта GNU, gdb. Это очень мощный отладчик, который распространяется бесплатно и может использоваться на многих платформах UNIX. Он также служит отладчиком по умолчанию в системах Linux. gdb перенесен на многие другие платформы и может применяться для отладки встроенных систем реального времени.

Запуск gdb

Перекомпилируйте программу примера для отладки и запустите gdb:

$ cc-g -o debug3 debug3.c

$ gdb debug3

GNU gdb 6.6

Copyright (C) 2006 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you

are welcome to change it and/or distribute copies of it under certain

conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for

details.

This GDB was configured as "i586-suse-linux"...

Using host libthread_db library "/lib/libthread_db.so.1".

(gdb)

У gdb есть довольно подробная интерактивная система помощи и полное справочное руководство, представляемое как набор файлов, которые можно просматривать с помощью программы info или из редактора Emacs.

(gdb) help

List of classes of commands:

aliases -- Aliases of other commands

breakpoints -- Making program stop at certain points

data -- Examining data

files -- Specifying and examining files

internals -- Maintenance commands

obscure -- Obscure features

running -- Running the program

stack -- Examining the stack

status -- Status inquiries

support -- Support facilities

tracepoints -- Tracing of program execution without stopping the program

user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.

Type "help all" for the list of all commands.

Type "help" followed by command name for full documentation.

Type "apropos word" to search for commands related to "word".

Command name abbreviations are allowed if unambiguous,

(gdb)

Сам по себе отладчик gdb — приложение, выполняющееся в текстовом режиме, но он предоставляет несколько сокращенных клавишных команд для выполнения повторяющихся задач. Во многих версиях есть редактирование в командной строке с хронологией команд, так что вы можете прокрутить список назад и выполнить ту же команду снова (попробуйте воспользоваться клавишами перемещения курсора). Все версии отладчика поддерживают "пустую команду"; нажатие клавиши <Enter> выполняет последнюю команду еще раз. Это особенно удобно при проверке выполнения программы в построчном режиме с помощью команд step или next.

Для завершения работы gdb применяйте команду quit.

Выполнение программы

Выполнить программу можно с помощью команды run. Любые аргументы, переданные вами команде run, пересылаются в программу как ее собственные аргументы. В данном случае вам не нужны никакие аргументы.

Предположим, что ваша система, как и системы обоих авторов, теперь генерирует сообщение о нарушении сегментации памяти. Если нет, читайте дальше. Вы узнаете, что делать, когда одна из ваших программ действительно сгенерирует сообщение о нарушении сегментации. Если вы не получили такого сообщения, но хотите поработать с этим примером во время чтения книги, когда первая из проблем, связанных с доступом к памяти, будет устранена, можно взять программу из файла debug4.c.

(gdb) run

Starting program: /home/neil/BLP4e/chapter10/debug3

Program received signal SIGSEGV, Segmentation fault. 

0x0804846f in sort (a=0x804a040, n=5) at debug3.c:23

23 /* 23 */ if(a[j].key > a[j+1].key) {

(gdb)

Программа, как и прежде, выполняется неверно. Когда программа дает сбой, gdb указывает причину и местонахождение. Теперь вы можете выяснять первопричину проблемы.

В зависимости от ядра вашей системы, версий библиотеки С и компилятора сбой программы может произойти в другом месте, например в строке 25, когда элементы массива меняются местами, а не в строке 23, когда сравниваются поля key элементов массива. Если это так, вы увидите следующее сообщение:

Program received signal SIGSEGV, Segmentation fault.

0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25

25 /* 25 */ a[j] = a[j+1];

Вы все равно можете продолжать следить за примером сеанса работы gdb, который описывается далее.

Трассировка стека

Программа была остановлена при выполнении функции sort в строке 23 исходного файла debug3.c. Если при компиляции вы не включили в программу дополнительную отладочную информацию (cc -g), то не сможете увидеть, где программа дала сбой, и использовать имена переменных для просмотра данных.

Увидеть, как вы добрались до этого места, можно с помощью команды backtrace:

(gdb) backtrace

#0 0x0804846f in sort (a=0x804a040, n=5) at debug3.c:23

#1 0x08048583 in main() at debug3.c:37

(gdb)

Это очень простая программа и трассировка у нее короткая, т.к. вы не вызывали много функций из других функций. Вы только видите, что sort была вызвана из main в строке 37 того же файла debug3.c. Обычно проблема гораздо сложнее, и команда backtrace применяется для определения маршрута, который привел к месту ошибки. Эта информация очень полезна при отладке функций, вызываемых из множества разных мест.

У команды backtrace есть сокращенная форма bt и для совместимости с другими отладчиками есть команда where, выполняющая ту же функцию.

Просмотр переменных

Отладчик вывел данные в момент остановки программы, и в трассировке стека показаны значения аргументов функции.

Функция sort была вызвана с параметром а, значение которого 0х804а040. Это адрес массива. Обычно он в различных системах разный и зависит от используемых компилятора и операционной системы.

Сбойная строка 23 — сравнение одного элемента массива с другим:

/* 23 */ if (a[j].key > a[j+1].key) {

Отладчик можно применять для просмотра содержимого параметров функции, локальных переменных и глобальных данных. Команда print отображает содержимое переменных и других выражений:

(gdb) print j

$1 = 4

Вы видите, что у локальной переменной j значение 4. Любые значения, выводимые командами gdb, подобными данной, сохраняются для будущего использования в псевдопеременных. В данном случае переменной $1 присвоено значение 4, на случай, если она вам позже понадобится. Последующие команды будут сохранять свои результаты в переменных $2, $3 и т.д.

Значение переменной j, равное 4, означает, что программа попыталась выполнить оператор

if (а[4].key > а[4+1].key)

У массива array, который вы передали функции sort, только пять элементов, которые пронумерованы от 0 до 4. Поэтому данный оператор считывает несуществующий элемент массива array[5]. Переменная цикла j приняла некорректное значение.

Если ваша программа завершилась в строке 25, система обнаружила чтение за пределами массива, только когда взялась за перестановку элементов массива, выполнив оператор

/* 25 */ а[j] = a[j+1];

который при j, равной 4, дает в результате

а[4] = а[4+1];

Просмотреть элементы передаваемого массива можно, применив выражение в команде print. В программе gdb вы можете использовать почти любое допустимое выражение языка С для вывода значения переменной, элемента массива или указателя.

(gdb) print а[3]

$2 = {data = "alex", '\0' <repeats 4091 times>, key = 1}

(gdb)

Отладчик gdb сохраняет результаты выполнения команд в псевдопеременных вида $<номер>. Результат последней команды всегда хранится в псевдопеременной $, а предыдущей — в $$. Это позволяет результат одной команды использовать в другой. Например:

(gdb) print j

$3 = 4

(gdb) print a[$-1].key

$4 = 1

Вывод листинга программы

Вы можете в программе gdb вывести на экран исходный текст программы с помощью команды list. Она выводит фрагмент кода, расположенного рядом с текущей позицией. Последующие вызовы list выведут остальной текст. Команде list можно задать в качестве аргумента имя функции, и команда отобразит фрагмент текста в этом месте программы, или можно указать пару номеров строк, и на экране появится текст программы, находящийся между этими строками.

(gdb) list

18 /* 18 */  int s = 1;

19 /* 19 */

20 /* 20 */  for(; i < n && s != 0; i++) {

21 /* 21 */   s = 0;

22 /* 22 */   for(j = 0; j < n; j++) {

23 /* 23 */    if(a[j].key > a[j+1].key) {

24 /* 24 */    item t = a[j];

25 /* 25 */    a[j] = a[j+1];

26 /* 26 */    a[j+1] = t;

27 /* 27 */    s++;

(gdb)

В строке 22 задано выполнение цикла до тех пор, пока переменная j меньше n. В данном случае n равна 5, поэтому у j будет последнее значение 4, слишком большое. Значение 4 приводит к сравнению а[4] с а[5] и возможной их перестановке. Единственное решение этой конкретной проблемы — исправить условие завершения цикла на следующее: j < n-1.

Давайте внесем это изменение, назовем новую программу debug4.c, откомпилируем ее и попробуем снова выполнить.

/* 22 */   for(j = 0; j < n-1; j++) {

$ cc -g -o debug4 debug4.с

$ ./debug4

array[0] = {john, 2}

array[1] = {alex, 1}

array[2] = {bill, 3}

array[3] = {neil, 4}

array[4] = {rick, 5}

Программа все еще не работает, поскольку она вывела неверно отсортированный список. Попробуем применить gdb для пошагового выполнения программы.

Установка точек останова

Для обнаружения места сбоя в программе необходимо иметь возможность проследить за тем, что делает программа во время выполнения. Остановить ее в любой момент можно с помощью точек останова. Они останавливают программу и передают управление отладчику. Вы сможете проверить переменные и затем разрешить программе продолжить выполнение.

В функции sort есть два цикла. Внешний цикл с переменной цикла i выполняется для каждого элемента массива один раз. Внутренний или вложенный цикл меняет местами элемент с последующим, расположенным ниже в списке. Это создает эффект всплытия пузырьков, поднимая вверх меньшие элементы. После каждого выполнения внешнего цикла самый большой элемент опускается на дно. Вы можете убедиться в этом, остановив программу на внешнем цикле и просмотрев состояние массива.

Для установки точек останова применяется ряд команд. Их перечень получен отладчиком gdb с помощью команды help breakpoint:

(gdb) help breakpoint

Making program stop at certain points.

List of commands:

awatch -- Set a watchpoint for an expression

break -- Set breakpoint at specified line or function

catch -- Set catchpoints to catch events

clear -- Clear breakpoint at specified line or function

commands -- Set commands to be executed when a breakpoint is hit

condition -- Specify breakpoint number N to break only if COND is true

delete -- Delete some breakpoints or auto-display expressions

delete breakpoints -- Delete some breakpoints or auto-display expressions

delete checkpoint -- Delete a fork/checkpoint (experimental)

delete mem -- Delete memory region

delete tracepoints -- Delete specified tracepoints

disable -- Disable some breakpoints

disable breakpoints -- Disable some breakpoints

disable display -- Disable some expressions to be displayed when program stops

disable mem -- Disable memory region

disable tracepoints -- Disable specified tracepoints

enable -- Enable some breakpoints

enable delete -- Enable breakpoints and delete when hit

enable display -- Enable some expressions to be displayed when program stops

enable mem -- Enable memory region

enable once -- Enable breakpoints for one hit

enable tracepoints -- Enable specified tracepoints

hbreak -- Set a hardware assisted breakpoint

ignore -- Set ignore-count of breakpoint number N to COUNT

rbreak -- Set a breakpoint for all functions matching REGEXP

rwatch -- Set a read watchpoint for an expression

tbreak -- Set a temporary breakpoint

tcatch -- Set temporary catchpoints to catch events

thbreak -- Set a temporary hardware assisted breakpoint

watch -- Set a watchpoint for an expression

Type "help" followed by command name for full documentation.

Type "apropos word" to search for commands related to "word".

Command name abbreviations are allowed if unambiguous.

Установите точку останова в строке 21 и выполните программу:

$ gdb debug4

(gdb) break 21

Breakpoint 1 at 0x8048427: file debug4.c, line 21.

(gdb) run

Starting program: /home/neil/BLP4e/chapter10/debug4

Breakpoint 1, sort (a=0x804a040, n=5) at debug4.c:21

21 /* 21 */    s = 0;

Вы можете вывести значение массива и затем с помощью команды cont разрешить программе продолжить выполнение. Это позволит программе выполняться до тех пор, пока она не натолкнется на следующую точку останова, в нашем случае это снова строка 21. В любой момент времени может быть активно несколько точек останова:

(gdb) print array[0]

$1 = (data = "bill", '\0' <repeats 4091 times>, key = 3)

Для вывода нескольких последовательных элементов массива можно применить конструкцию @<число>, чтобы заставить gdb вывести указанное количество элементов массива. Для того чтобы вывести все пять элементов, можно использовать следующую команду:

(gdb) print array[0]@5

$2 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, {

    data = "neil", '\0' <repeats 4091 times>, key =4}, {

    data = "john", '\0' <repeats 4091 times>, key =2}, {

    data = "rick", '\0' <repeats 4091 times>, key =5}, {

    data = "alex", '\0' <repeats 4091 times>, key = 1}}

Учтите, что вывод немного подчищен, чтобы его легче было читать. Поскольку это первый проход цикла, массив еще не изменен. Когда вы разрешите программе продолжить выполнение, то увидите последовательные перестройки массива array, происходящие по мере выполнения программы:

(gdb) cont

Continuing.

Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:21

21 /* 21 */   s = 0;

(gdb) print array[0]@5

$3 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, {

    data = "john", '\0' <repeats 4091 times>, key =2}, {

    data = "neil", '\0' <repeats 4091 times>, key = 4}, {

    data = "alex", '\0' <repeats 4091 times>, key =1}, {

    data = "rick", '\0' <repeats 4091 times>, key =5}}

(gdb)

Можно воспользоваться командой display, чтобы задать в gdb автоматическое отображение массива при каждой остановке программы в точке останова:

(gdb) display array[0]@5

1: array[0]@5 = {{data = "bill", '\0' <repeats 4091 times>, key = 3}, {

    data = "john", '\0' <repeats 4091 times>, key = 2}, {

    data = "neil", '\0' <repeats 4091 times>, key = 4}, {

    data = "alex", '\0' <repeats 4091 times>, key = 1}, {

    data = "rick", '\0' <repeats 4091 times>, key, = 5}}

Более того, вы можете изменить точку останова таким образом, что вместо остановки программы она просто отобразит данные, которые вы запросили, и продолжит выполнение. Для этого примените команду commands. Она позволит указать, какие команды отладчика выполнять при попадании в точку останова. Поскольку вы уже указали отображение, вам нужно лишь задать команду в точке останова для продолжения выполнения:

(gdb) commands

Type commands for when breakpoint 1 is hit, one per line.

End with a line saying just "end".

> cont

> end

Теперь, когда вы разрешите программе продолжить выполнение, она продолжается до завершения, выводя значение массива каждый раз, когда оказывается вблизи внешнего цикла.

(gdb) cont

Continuing.

Breakpoint 1, sort (a=0x8049684, n=3) at debug4.c:21

21 /* 21 */    s = 0;

1: array[0]@5 = {{data = "john", '\000' <repeats 4091 times>, key = 2}, {

    data = "bill", '\000' <repeats 4091 times>, key =3}, {

    data = "alex", '\000' <repeats 4091 times>, key =1}, {

    data = "neil", '\000' <repeats 4091 times>, key =4}, {

    data = "rick", '\000' <repeats 4091 times>, key = 5}}

array[0] = {john, 2}

array[1] = {alex, 1}

array[2] = {bill, 3}

array[3] = {neil, 4}

array[4] = {rick, 5}

Program exited with code 025.

(gdb)

Отладчик gdb сообщает о том, что программа завершается с необычным кодом завершения. Это происходит потому, что программа сама не вызывает exit и не возвращает значение из функции main. Код завершения в данном случае не имеет смысла, значимый код должен предоставляться вызовом функции exit.

Кажется, что программа не выполняет внешний цикл столько раз, сколько ожидалось. Вы можете увидеть, что значение параметра n, используемого в условии завершения цикла, уменьшается при каждом достижении точки останова. Это значит, что цикл не будет выполняться нужное число раз. Дело в уменьшении n в строке 30.

/* 30 */   n--;

Это попытка оптимизировать программу за счет того, что в конце каждого прохода внешнего цикла наибольший, элемент array окажется внизу и поэтому остается меньше элементов для сортировки. Но как видно, это мешает внешнему циклу и создает проблемы. Простейший способ исправления (хотя есть и другие) — удалить ошибочную строку. Давайте проверим, применив отладчик для корректировки, устранило ли такое исправление проблему.

Вставка исправлений с помощью отладчика

Вы уже видели, что можно применять отладчик для установки точек останова и просмотра значений переменных. Применив точки останова с заданными действиями, можно проверить исправление, называемое "заплатой", перед тем, как изменять текст программы и выполнять ее повторную компиляцию. В данном случае нужно остановить программу в строке 30 и увеличить переменную n. В дальнейшем, когда строка 30 выполнится, значение останется неизменным.

Давайте перезапустим программу с самого начала. Прежде всего вы должны удалить вашу точку останова и отладочный вывод. С помощью команды info можно увидеть, какие точки останова и какой вывод вы включили:

(gdb) info display

Auto-display expressions now in effect:

Num Enb Expression

1: y array[0]@5 (gdb) info break

Num Type       Disp Enb Address    What

1   breakpoint keep y   0x08048427 in sort at debug4.c:21

    breakpoint already hit 3 times

    cont

Вы можете либо отключить эти точки останова, либо удалить их совсем. Если их отключить, у вас останется возможность включить их позже, когда понадобится.

(gdb) disable break 1

(gdb) disable display 1

(gdb) break 30

Breakpoint 2 at 0x8048545: file debug4.c, line 30.

(gdb) commands 2

Type commands for when breakpoint 2 is hit, one per line.

End with a line saying just "end".

>set variable n = n+1

>cont

>end

(gdb) run

Starting program: /home/neil/BLP4e/chapter10/debug4

Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30

30 /* 30 */   n--;

Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30

30 /* 30 */   n--;

Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30

30 /* 30 */   n--;

Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30

30 /* 30 */   n--;

Breakpoint 2, sort (a=0x804a040, n=5) at debug4.c:30

30 /* 30 */   n--;

array[0] = {alex, 1}

array[1] = {john, 2}

array[2] = {bill, 3}

array[3] = {neil, 4}

array[4] = {rick, 5}

Program exited with code 025.

(gdb)

Программа выполняется полностью и выводит корректный результат. Теперь можно внести изменения и переходить к тестированию ее с большим объемом данных.

Дополнительные сведения о gdb

Отладчик проекта GNU — исключительно мощный инструмент, способный снабжать множеством сведений о внутреннем состоянии выполняющихся программ. В системах, поддерживающих средство аппаратно устанавливаемых контрольных точек, можно применять gdb для наблюдения за изменениями переменных в режиме реального времени. Аппаратно устанавливаемые контрольные точки — это функция некоторых ЦПУ; такие процессоры способны автоматически останавливаться при возникновении определенных условий, обычно доступе к памяти в заданной области. Кроме того, gdb может следить (watch) за выражениями. Это означает, что с потерей производительности gdb может остановить программу, когда выражение принимает конкретное значение, независимо от того, в каком месте программы выполнялось вычисление.

Точки останова можно устанавливать со счетчиками и условиями, так что они включаются только после фиксированного числа проходов или при выполнении условия.

Отладчик gdb также способен подключаться к уже выполняющимся программам. Это очень полезно при отладке клиент-серверных систем, поскольку вы сможете отлаживать некорректно ведущий себя серверный процесс во время выполнения без необходимости останавливать и перезапускать его. Можно компилировать программы, например, с помощью строки gcc -O -g, чтобы получить преимущества от применения оптимизации и отладочной информации. Недостаток заключается в том, что оптимизация может слегка переупорядочить текст программы, поэтому, когда вы будете выполнять программу в пошаговом режиме, может оказаться, что вы "скачете вперед и назад" по строкам, чтобы добиться того эффекта, что и в первоначальном тексте программы.

Отладчик gdb можно также применять для отладки аварийно завершившихся программ. Системы Linux и UNIX при аварийном завершении программы часто создают дамп ядра в файле с именем core. Это отображение карты памяти программы, которое содержит значения глобальных переменных в момент возникновения сбоя. Вы сможете использовать gdb для того, чтобы определить место в программе, вызвавшее аварийное завершение. Дополнительную информацию см. в интерактивном справочном руководстве к gdb.

Отладчик gdb доступен в соответствии с требованиями Общедоступной лицензии проекта GNU и его поддерживает большинство систем UNIX. Мы настоятельно рекомендуем вам, как следует изучить его.

Дополнительные средства отладки

Помимо полнофункциональных отладчиков, таких как gdb, Linux-системы обычно предоставляют и другие средства, которые можно применять для поддержки процесса отладки. Некоторые из них снабжают статической информацией о программе, другие обеспечивают динамический анализ.

Статический анализ предоставляет сведения только об исходном тексте программы. Программы ctags, cxref и cflow работают с исходными файлами и предлагают полезные данные о вызовах функций и их месте в программе.

Динамический анализ предоставляет информацию о том, как программа ведёт себя во время выполнения. Программы prof и gprof предлагают сведения о том, какие функции были выполнены, и сколько времени заняло их выполнение,

Давайте рассмотрим некоторые из этих средств и их вывод. Не все они будут доступны во всех системах, хотя у многих из этих средств есть свободно распространяемые версии.

Lint удаление ошибок из ваших программ

Первые системы UNIX предоставляли утилиту lint. Эта программа по существу — препроцессор компилятора С со вставленными тестами, обеспечивающими некоторые проверки с точки зрения здравого смысла и вывод предупреждений. Среди прочего она обнаруживает случаи применения переменных до того, как им было присвоено значение, или случаи неиспользования аргументов функций.

Более современные компиляторы C могут ценой производительности времени компиляции формировать аналогичные предупреждения. Утилиту lint, как таковую, обогнала стандартизация языка С. Поскольку средство основывалось на раннем компиляторе С, оно совсем не справляется с синтаксисом ANSI. Есть несколько коммерческих версий lint для UNIX и одна версия в Интернете для Linux, названная splint. Она известна под именем LClint, как часть проекта MIT (Massachusetts Institute of Technology, Массачусетский технологический институт), занимающегося разработкой средств формального описания. splint, средство подобное lint, может предоставлять полезные обзорные комментарии к программному коду. Найти splint можно по адресу http://www.splint.org.

Далее приведена первоначальная версия (debug0.c) программы-примера, которую вы уже отладили.

/*  1 */ typedef struct {

/*  2 */  char *data;

/*  3 */  int key;

/*  4 */ } item;

/*  5 */

/*  6 */ item array[j] = {

/*  7 */  {"bill", 3},

/*  8 */  {"neil", 4},

/*  9 */  {"john", 2},

/* 10 */  {"rick", 5},

/* 11 */  {"alex", 1},

/* 12 */ };

/* 13 */

/* 14 */ sort(a, n)

/* 15 */ item *a;

/* 16 */ {

/* 17 */  int i = 0, j = 0;

/* 18 */  int s;

/* 19 */

/* 20 */  for(; i < n & s != 0; i++) {

/* 21 */   s = 0;

/* 22 */   for(j = 0; j < n; j++) {

/* 23 */    if(a[j].key > a[j+1].key) {

/* 24 */     item t = a[j];

/* 25 */     a[j] = a[j+1];

/* 26 */     a[j+1] = t;

/* 27 */     s++;

/* 28 */    }

/* 29 */   }

/* 30 */   n--;

/* 31 */  }

/* 32 */ }

/* 33 */

/* 34 */ main()

/* 35 */ {

/* 36 */  sort(array,5);

/* 37 */ }

В этой версии есть проблема в строке 20, где вместо предполагаемого оператора && применяется оператор &. Далее приведен отредактированный пример вывода splint, выполненной с этой версией программы. Обратите внимание на то, как она обнаруживает проблемы в строке 20 — тот факт, что вы не инициализировали переменную s и что возможны проблемы с условием из-за некорректного оператора.

neil@susel03:~/BLP4e/chapter10> splint -strict debug0.c

Splint 3.1.1 --- 19 Mar 2005

debug0.c:7:18: Read-only string literal storage used as initial value for

               unqualified storage: array[0].data = "bill"

A read-only string literal is assigned to a non-observer reference. (Use -readonlytrans to inhibit warning)

debug0.c:8:18: Read-only string literal storage used as initial value for

               unqualified storage: array[1].data = "neil"

debug0.c:9:18: Read-only string literal storage used as initial value for

               unqualified storage: array[2].data = "john"

debug0.с:10:18: Read-only string literal storage used as initial value for

               unqualified storage: array[3].data = "rick"

debug0.c:11:18: Read-only string literal storage used as initial value for

               unqualified storage: array[4].data = "alex"

debug0.с:14:22: Old style function declaration

 Function definition is in old style syntax. Standard prototype syntax is

 preferred. (Use -oldstyle to inhibit warning)

debug0.с: (in function sort)

debug0.c:20:31: Variable s used before definition

 An rvalue is used that may not be initialized to a value on some execution

 path. (Use -usedef to inhibit warning)

debug0.с:20:23: Left operand of & is not unsigned value (boolean):

               i < n & s != 0

 An operand to a bitwise operator is not an unsigned values. This may have

 unexpected results depending on the signed representations. (Use

 -bitwisesigned to inhibit warning).

debug0.c:20:23: Test expression for for not boolean, type unsigned int:

               i < n & s != 0

 Test expression type is not boolean or int. (Use -predboolint to inhibit

 warning);

debug0.с:25:41: Undocumented modification of a[]: a[j] = a[j + 1]

 An externally-visible object is modified by a function with no /*@modifies@*/

 comment. The /*@modifies ... @*/ control comment can be used to give a

 modifies list for an unspecified function. (Use -modnomods to inhibit

 warning)

debug0.c:26:41: Undocumented modification of a[]: a[j + 1] = t

debug0.c:20:23: Operands of & are non-integer (boolean) (in post loop test):

               i < n & s != 0

 A primitive operation does not type check strictly. (Use -strictops to

 inhibit warning)

debug0.с:32:14: Path with no return in function declared to return int

 There is a path through a function declared to return a value on which there

 is no return statement. This means the execution may fall through without

 returning a meaningful result to the caller. (Use -noret to inhibit

 warning)

debug0.с:34:13: Function main declared without parameter list

 A function declaration does not have a parameter list. (Use -noparams

 to inhibit warning)

debug0.с: (in function main)

debug0.с:36:22: Undocumented use of global array

 A checked global variable is used in the function, but not listed in its

 globals clause. By default, only globals specified in .lcl files are

 checked.

 To check all globals, use +allglobals. To check globals selectively use

 /*@checked@*/ in the global declaration. (Use -globs to inhibit warning)

debug0.с:36:17: Undetected modification possible from call to unconstrained

               function sort: sort

 An unconstrained function is called in a function body where

 modifications are checked. Since the unconstrained function may modify

 anything, there may be undetected modifications in the checked function.

 (Use -modunconnomods to inhibit warning)

debug0.c:36:17: Return value (type int) ignored: sort(array, 5)

 Result returned by function call is not used. If this is intended, can

 cast result to (void) to eliminate message. (Use -retvalint to inhibit

 warning)

debug0.c:37:14: Path with no return in function declared to return int

debug0.c:6:18: Variable exported but not used outside debug0: array

 A declaration is exported, but not used outside this module. Declaration

 can use static qualifier. (Use -exportlocal to inhibit warning)

debug0.c:14:13: Function exported but not used outside debug0: sort

 debug0.c:15:17: Definition of sort

debug0.c:6:18: Variable array exported but not declared in header file

 A variable declaration is exported, but does not appear in a header

 file. (Used with exportheader.) (Use -exportheadervar to inhibit warning)

debug0.c:14:13: Function sort exported but not declared in header file

 A declaration is exported, but does not appear in a header file. (Use

 -exportheader to inhibit warning)

debug0.c:15:17: Definition of sort

Finished checking - 22 code warnings

$

Утилита выражает неудовольствие по поводу объявления функций в старом стиле (не ANSI) и несоответствия типов значений, возвращаемых функциями, и самими величинами, которые они возвращают (или нет) в действительности. Эти предупреждения не влияют на работу программы, но должны быть выведены.

Она также обнаружила две реальные ошибки в следующем фрагменте кода:

/* 18 */  int s;

/* 19 */

/* 20 */  for(; i < n & s != 0; i++) {

/* 21 */   s = 0;

Средство splint определило (выделенные цветом строки предыдущего вывода), что переменная s используется в строке 20, но не была при этом инициализирована, и что оператор & стоит на месте более обычного оператора &&. В данном случае старшинство оператора изменяет значение условия и создает проблему в программе.

Обе эти ошибки были исправлены при чтении исходного текста программы до запуска процесса отладки. Несмотря на то, что пример мало изобретателен и служит только для демонстрации, подобные ошибки регулярно возникают в реальных программах."

Средства, отслеживающие вызовы функций

Три утилиты — ctags, cxref и cflow — формируют часть стандарта X/Open и, следовательно, должны включаться в системы, представляемые как системы UNIX с программными средствами разработки.

Примечание

Эти утилиты и другие, упоминаемые в этой главе, могут не входить в состав вашего дистрибутива Linux. Если они пропущены, можно поискать их реализации в Интернете. Хорошая отправная точка (для дистрибутивов Linux, поддерживающих формат RPM-пакетов) — Web-сайты http://rpmfind.net и http://rpm.pbone.net. Можно попытаться поискать в нескольких репозитариях для конкретных дистрибутивов, включая http://ftp.gwdg.de/pub/opensuse/ для openSUSE, http://rpm.livna.org для Fedora и http://packages.slackware.it/ для Slackware.

ctags

Программа ctags создает алфавитный указатель функций. Для каждой функции вы получаете перечень мест в программе, где она применяется, как алфавитный указатель к книге.

ctags [-a] [-f filename] sourcefile sourcefile ...

ctags -x sourcefile sourcefile ...

По умолчанию ctags создает в текущем каталоге файл с именем tags, содержащий для каждой функции, объявленной в любом из входных файлов исходного кода, строки следующего вида:

announce app_ui.c /^static void announce(void) /

Каждая строка файла содержит имя функции, файл, в котором она объявлена, и регулярное выражение, которое можно использовать для поиска описания функции в файле. Некоторые редакторы, например Emacs, могут применять файлы этого вида для навигации в исходном тексте программы.

Кроме того, с помощью опции в программе ctags (если она доступна в вашей версии программы) вы можете формировать строки аналогичного вида в стандартном файле вывода.

find_cat 403 appui.с static cdc_entry find_cat(

Можно перенаправить вывод в другой файл с помощью опции -f filename и добавить его в конец существующего файла, указав опцию .

cxref

Программа cxref анализирует исходный текст на языке С и формирует перекрестные ссылки. Она показывает, где в программе упоминается каждое символическое имя (переменная, директива #define и функция). Программа создает отсортированный список с указанием места определения каждого идентификатора, которое помечается звездочкой, как показано далее:

 SYMBOL                FILE  FUNCTION LINE

 BASENID               prog.с      --  *12 *96 124 126 146 156 166

 BINSIZE               prog.с      --  *30 197 198 199. 206

  BUFMAX               prog.с      --  *44 45 90

  BUFSIZ /usr/include/stdio.h      --  *4

     EOF /usr/include/stdio.h      --  *27

    argc               prog.с      --  36

                       prog.с    main  *37 61 81

    argv               prog.с      --  36

                       prog.с    main  *38 61

calldata               prog.с      --  *5

                       prog.с    main  64 188

  calls                prog.с      --  *19

                       prog.с     main 54

На машине одного из авторов этой книги предыдущий вывод был сгенерирован в каталоге с исходными файлами приложения с помощью команды

$ cxref *.с *.h

но точный синтаксис зависит от версии. См. документацию к вашей системе и интерактивное справочное руководство для получения дополнительной информации о том, включена ли программа cxref и как ее применять.

cflow

Программа cflow выводит дерево вызовов функций — схему, показывающую, какие функции вызывают другие функции, какие функции вызываются этими другими и т.д. Эта схема полезна для выяснения структуры программы, понимания ее принципов действия и наблюдения за влиянием изменений, внесенных в функцию. Некоторые версии программы cflow могут работать с объектными файлами так же, как с исходными. Подробности см. в интерактивном справочном руководстве.

Далее приведен пример вывода, полученный версией cflow (cflow-2.0), которая есть в Интернете и поддерживается Марти Лейснером (Marty Leisner).

0  file_ungetc {prcc.c 997}

1  main {prcc.c 70}

2      getopt {}

3      show_all_lists {prcc.c 1070}

4          display_list {prcc.c 1056}

5              printf {}

6          exit {}

7      exit {}

9      usage {prcc.c 59}

10         fprintf {}

11         exit {}

Пример информирует о том, что функция main вызывает (среди прочих) функцию show_all_lists и что show_all_lists в свою очередь вызывает функцию display_list, которая вызывает функцию printf.

У этой версии cflow есть опция -i, которая формирует инвертированный потоковый граф. Утилита cflow перечисляет для каждой функции другие функции, вызывающие данную. Звучит не очень понятно, но на самом деле все просто. Далее приведен пример:

19  display_list {prcc.c 1056}

20      show_all_lists {prcc.c 1070}

21  exit {}

22      main {prcc.c 70}

23      show_all_lists {prcc.c 1070}

24      usage {prcc.c 59}

25  ...

74  printf {}

75      display_list {prcc.c 1056}

76      maketag {prcc.c 4 87}

77  show_all_lists {prcc.c 1070}

78      main {prcc.c 70}

79  ...

99  usage {prcc.c 59}

100     main {prcc.c 70}

В примере показано, что функцию exit, например, вызывают функции main, show_all_lists и usage.

Выполнение профилирования с помощью prof/gprof

Методика, зачастую полезная при попытках выяснить проблемы снижения производительности программы, называется профилированием выполнения (execution profiling). Профиль программы, обычно поддерживаемый специальными опциями компилятора и вспомогательными программами, показывает, где программа тратит время.

Программа prof (и ее эквивалент в проекте GNU, gprof) выводит отчёт из файла трассировки выполнения, который формируется во время выполнения профилируемой программы. Профилируемый исполняемый файл создается с помощью флага компилятора -p (для prof) или флага -pg (для gprof).

$ cc -pg -о program program.с

Программа компонуется со специальной библиотекой С, и в нее включается контрольный код. В конкретных системах он может отличаться, но общая цель — такая организация программы, которая позволяет часто прерывать выполнение и записывать этап выполнения. Контрольные данные записываются в файл mon.out (gmon.out для gprof) в текущем каталоге.

$ ./program

$ ls -ls

2 -rw-r--r-- 1 neil users 1294 Feb 4 11:48 gmon.out

Программа prof/gprof читает эти контрольные данные и выводит отчет. См. подробности, касающиеся опций программы, в интерактивном справочном руководстве. Далее в качестве примера приведен вывод (сокращенный) программы gprof.

cumulative  self    self   total

   time    seconds seconds  calls ms/call ms/call            name

   18.5       0.10    0.10   8664    0.01    0.03      doscan [4]

   18.5       0.20    0.10                            mcount (60)

   14.8       0.28    0.08  43320    0.00    0.00     _number [5]

    9.3       0.33    0.05   8664    0.01    0.01 _format_arg [6]

    7.4       0.37    0.04 112632    0.00    0.00     _ungetc [8]

    7.4       0.41    0.04   8757    0.00    0.00    _memccpy [9]

    7.4       0.45    0.04      1   40.00  390.02       _main [2]

    3.7       0.47    0.02     53    0.38    0.38      _read [12]

    3.7       0.49    0.02                             w4str [10]

    1.9       0.50    0.01  26034    0.00    0.00    _strlen [16]

    1.9       0.51    0.01   8664    0.00    0.00    strncmp [17]

Проверки соблюдения условий

Несмотря на то, что вставка на этапе разработки программы с помощью условной компиляции отладочного кода, такого как вызовы printf, распространена, иногда оставлять такие сообщения в поставляемой программе непрактично. Но часто проблемы возникают во время работы программы из-за некорректных допущений или исходных данных, а не из-за ошибок кодирования. Это события, которых "не может быть никогда". Например, функция может быть написана в расчете на то, что ее входные параметры будут в определенном диапазоне. Если передать ей некорректные данные, она может сделать некорректной работу всей системы.

В тех случаях, когда внутренняя логика системы нуждается в подкреплении, X/Open предоставляет макрос assert, применяемый для проверки правильности исходных данных и остановки выполнения программы в противном случае.

#include <assert.h>

void assert(int expression)

Макрос assert вычисляет выражение и, если оно не равно нулю, выводит некоторую диагностическую информацию о стандартной ошибке и вызывает функцию abort для завершения программы.

Заголовочный файл assert.h определяет макросы в зависимости от определения флага NDEBUG. Если NDEBUG определен во время обработки заголовочного файла, assert определяется по существу как ничто. Это означает, что вы можете отключить проверки заданных выражений во время компиляции, компилируя с опцией -DNDEBUG или вставив перед включением файла assert.h строку

#define NDEBUG

в каждый исходный файл.

Этот метод применения порождает проблему. Если вы используете assert во время тестирования, но отключите макрос в рабочем коде, в вашем рабочем коде может оказаться менее строгая проверка, чем применявшаяся в процессе его тестирования. Обычно макросы assert не оставляют включенными в рабочем коде — вряд ли вам понравится рабочий код, предоставляющий пользователю недружелюбное сообщение assert failed и останавливающий программу. Быть может, лучше написать свою отслеживающую ошибки подпрограмму, которая проверяет выражение, использовавшееся в макросе, но не нуждается в полном отключении в рабочем коде.

Вы также должны убедиться в том, что у выражения макроса assert нет побочных эффектов. Например, если вы применяете вызов функции с побочным эффектом, этот побочный эффект не проявится в рабочем коде с отключенными макросами assert.

Выполните упражнение 10.2.

Упражнение 10.2. Программа assert.c.

Далее приведена программа assert.c, определяющая функцию, которая должна принимать положительное значение. Она защищает от ввода некорректного аргумента благодаря применению макроса assert.

После включения заголовочного файла assert.h и функции "квадратный корень", проверяющей положительное значение параметра, вы можете писать функцию main.

#include <stdio.h>

#include <math.h>

#include <assert.h>

#include <stdlib.h>

double my_sqrt(double x) {

 assert(x >= 0.0);

 return sqrt(x);

}

int main() {

 printf("sqrt +2 = %g\n", my_sqrt(2.0));

 printf("sqrt -2 = %g\n", my_sqrt(-2.0));

 exit(0);

}

Теперь при выполнении программы вы увидите нарушение в макросе assert при передаче некорректного значения. Точный формат сообщения о нарушении условия макроса assert в разных системах разный.

$ сс -о assert assert.с -lm

$ ./assert

sqrt +2 = 1.41421

assert: assert.c:7: my_sqrt: Assertion 'x >= 0.0' failed.

Aborted

$

Как это работает

Когда вы попытаетесь вызвать функцию my_sqrt с отрицательным числом, макрос assert даст сбой. Он предоставляет файл и номер строки, в которой нарушено условие и само нарушенное условие. Программа завершается прерыванием abort. Это результат вызова abort макросом assert.

Если вы перекомпилируете программу с опцией -DNDEBUG, макрос assert не компилируется, и вы получаете NaN (Not a Number, не число) — значение, указывающее на неверный результат при вызове функции sqrt из функции my_sqrt.

$ cc -о assert -DNDEBUG assert.с -lm

$ ./assert

sqrt +2 = 1.41421

sqrt -2 = nan

$

Некоторые более старые версии математической библиотеки генерируют исключение для математической ошибки, и ваша программа будет остановлена с сообщением "Floating point exception" ("Исключение для числа с плавающей точкой") вместо возврата NaN.

Устранение ошибок использования памяти

Распределение динамической памяти — богатый источник ошибок, которые трудно выявить. Если вы пишете программу, применяющую функции malloc и free для распределения памяти, важно внимательно следить за блоками, которые вы выделяете, и быть уверенным в том, что не используется блок, который вы уже освободили.

Обычно блоки памяти выделяются функцией malloc и присваиваются переменным-указателям. Если переменная-указатель изменяется, и нет других указателей, указывающих на блок памяти, он становится недоступным. Это утечка памяти, вызывающая увеличение размера программы. Если вы потеряете большой объем памяти, скорость работы вашей системы, в конце концов, снизится, и система уйдет за пределы памяти.

Если вы записываете в область, расположенную после конца выделенного блока (или перед началом блока), вы с большой долей вероятности повредите структуры данных, используемые библиотекой malloc, следящей за распределением памяти. В этом случае в какой-то момент времени вызов malloc или даже free приведет к нарушению сегментации, и ваша программа завершится аварийно. Определение точного места возникновения сбоя может оказаться очень трудной задачей, поскольку нарушение могло возникнуть задолго до события, вызвавшего аварийное завершение программы.

Неудивительно, что существуют коммерческие и бесплатные средства, способные помочь в решении проблем этих двух типов. Например, есть много разных версий функций malloc и free, которые содержат дополнительный код для проверки выделения и освобождения блоков памяти и пытаются учесть двойное освобождение блока и другие типы неправильного использования памяти.

ElectricFence

Библиотека ElectricFence была разработана Брюсом Перенсом (Bruce Perens). Она доступна как необязательный компонент в некоторых дистрибутивах Linux, таких как Red Hat (Enterprise и Fedora), SUSE и openSUSE, и может быть легко найдена в Интернете. Это средство пытается применять виртуальную память системы Linux для защиты памяти, используемой функциями malloc и free, и аварийного останова программы в момент повреждения памяти.

Выполните упражнение 10.3.

Упражнение 10.3. Применение библиотеки ElectricFence

Далее приведена программа efence.c, которая выделяет память с помощью функции malloc и пишет данные за концом выделенного блока. Познакомьтесь с ней и посмотрите, что произойдет.

#include <stdio.h>

#include <stdlib.h>

int main() {

 char *ptr = (char *)malloc(1024);

 ptr[0] = 0;

 /* Теперь пишет за пределы блока */

 ptr[1024] = 0;

 exit(0);

}

Когда вы откомпилируете и выполните программу, то не увидите некорректного поведения. Однако вероятно, что область памяти, выделенная malloc, повреждена, и вы, в конце концов, попадете в беду.

$ cc -о efence efence.с

$ ./efence

$

Тем не менее, если вы возьмете ту же самую программу и скомпонуйте ее с библиотекой ElectricFence (libefence.a), то получите немедленный отклик:

$ cc -о efence efence.с -lefence

$ ./efence

Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens <bruce@perens.com>

Segmentation fault

$

Выполнение под контролем отладчика позволяет получить подробное описание проблемы;

$ cc -g -о efence efence.с -lefence

$ gdb efence

(gdb) run

Starting program: /home/neil/BLP4e/chapter10/efence

Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens bruce@perens.com

Program received signal SIGSEGV, Segmentation fault.

[Switching to Thread 1024 (LWP 1869)]

0x08048512 in main () at efence.c:10

10  ptr[1024] = 0;

(gdb)

Как это работает

Библиотека ElectricFence заменяет функцию malloc и связанные с ней функции версиями, применяющими аппаратные средства виртуальной памяти для защиты от несанкционированного доступа к памяти. При возникновении подобного обращения к памяти порождается сигнал нарушения сегментации и программа останавливается.

valgrind

Средство valgrind способно обнаруживать многие из обсуждавшихся нами проблем (упражнение 10.4). Прежде всего, оно умеет находить ошибки доступа, к массиву и утечки памяти. Это средство, возможно, не включено в ваш дистрибутив Linux, но его можно найти на Web-сайте http://valgrind.org.

Для применения valgrind даже не требуется перекомпиляции программы, и вы можете находить ошибки доступа к памяти в выполняющейся программе. Данное средство заслуживает внимания; оно применяется в основных разработках, включая среду KDE версии 3.

Упражнение 10.4. Средство valgrind

Далее приведена программа checker.c, которая выделяет некоторый объем памяти, читает область памяти и записывает данные за пределами выделенного участка, а затем делает выделенный участок недоступным.

#include <stdio.h>

#include <stdlib.h>

int main() {

 char *ptr = (char *)malloc(1024);

 char ch;

 /* Неинициализированное чтение */

 ch = ptr[1024];

 /* Запись за пределами блока */

 ptr[1024] = 0;

 /* Потеря блока */

 ptr = 0;

 exit(0);

}

Для применения valgrind вы просто выполняете команду valgrind, передав ей опции, задающие нужные виды проверок, и далее указав программу для выполнения с ее аргументами (если таковые есть).

При выполнении программы с valgrind вы увидите множество обнаруженных проблем:

$ valgrind --leak-check=yes -v ./checker

==4780== Memcheck, a memory error detector.

==4780== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al.

==4780== Using LibVEX rev 1732, a library for dynamic binary translation.

==4780== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.

==4780== Using valgrind-3.2.3, a dynamic binary instrumentation framework.

==4780== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.

==4780==

--4780-- Command line

--4780--    ./checker

--4780-- Startup, with flags:

--4780--    --leak-check=yes

--4780--    -v

--4780-- Contents of /рroc/version:

--4780-- Linux version 2-6.20.2-2-default (geeko@buildhost) (gcc version 4.1.3 20070218 (prerelease) (SUSE Linux)) #1 SMP Fri Mar 9 21:54:10 UTC 2007

--4780-- Arch and hwcaps: X86, x86-sse1-sse2

--4780-- Page sizes: currently 4096, max supported 4096

--4780-- Valgrind library directory: /usr/lib/valgrind

--4780-- Reading syms from /lib/ld-2.5.so (0x4000000)

--4780-- Reading syms from /home/neil/BLP4e/chapter10/checker (0x8048000)

--4780-- Reading syms from /usr/lib/valgrind/x86-linux/memcheck (0x38000000)

--4780--    object doesn't have a symbol table

--4780--    object doesn't have a dynamic symbol table

--4780-- Reading suppressions file: /usr/lib/valgrind/default.supp

--4780-- REDIR: 0x40158B0 (index) redirected to 0x38027EDB (???)

--4780-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_core.so (0x401E000)

--4780--    object doesn't have a symbol table

--4780-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so (0x4021000)

--4780--    object doesn't have a symbol table

==4780= WARNING: new redirection conflicts with existing -- ignoring it

--4780--    new: 0x040158B0 (index ) R-> 0x04024490 index

--4780-- REDIR: 0x4015A50 (strlen) redirected to 0x4024540 (strlen)

--4780-- Reading syms from /lib/libc-2.5.so (0x4043000)

--4780-- REDIR: 0x40ADFF0 (rindex) redirected to 0x4024370 (rindex)

--4780-- REDIR: 0x40AAF00 (malloc) redirected to 0x4023700 (malloc)

==4780== Invalid read of size 1

==4780==    at 0x804842C: main (checker.с: 10)

==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd

==4780==    at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so)

==4780==    by 0x8048420: main (checker.c: 6)

=4780=

==4780== Invalid write of size 1

==4780==    at 0x804843A: main (checker.с: 13)

==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd

==4780==    at 0x4 023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so)

==4780==    by 0x8048420: main (checker.c: 6)

--4780-- REDIR: 0x40A8BB0 (free) redirected to 0x402331A (free)

--4780-- REDIR: 0x40AEE70 (memset) redirected to 0x40248A0 (memset)

==4780==

==4780== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 3 from 1)

==4780==

==4780== 1 errors in context 1 of 2:

==4780== Invalid write of size 1

==4780==    at 0x804843A: main (checker.с: 13)

==4780== Address 0x4170428 is 0 bytes after a block of size 1,024 alloc'd

==4780==    at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so)

==4780==    by 0x80484 20: main (checker.c: 6)

==4780==

==4780== 1 errors in context 2 of 2:

==4780== Invalid read of size 1

==4780==    at 0x804842C: main (checker.c:10)

==4780== Address 0x4170428 is 0-bytes after a block of size 1,024 alloc'd

==4780==    at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so)

==4780==    by 0x8048420: main (checker.с: 6)

--4780--

--4780-- supp: 3 dl-hack3

==4780==

==4780== IN SUMMARY: 2 errors from 2 contexts (suppressed: 3 from 1)

==4780==

==4780== malloc/free: in use at exit: 1,024 bytes in 1 blocks.

==4780== malloc/free: 1 allocs, 0 frees, 1,024 bytes allocated.

==4780==

==4780== searching for pointers to 1 not-freed blocks.

==4780== checked 65,444 bytes.

==4780==

==4780==

==4780== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1

==4780==    at 0x4023785: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so)

==4780==    by 0x8048420: main (checker.c: 6)

==4780==

==4780== LEAK SUMMARY:

==4780==    definitely lost: 1,024 bytes in 1 blocks.

==4780==      possibly lost: 0 bytes in 0 blocks.

==4780==    still reachable: 0 bytes in 0 blocks.

==4780==         suppressed: 0 bytes in 0 blocks.

--4780--  memcheck: sanity checks: 0 cheap, 1 expensive

--4780--  memcheck: auxmaps: 0 auxmap entries (0k, 0M) in use

--4780--  memcheck: auxmaps: 0 searches, 0 comparisons

--4780--  memcheck: SMs: n_issued = 9 (144k, 0M)

--4780--  memcheck: SMs: n_deissued = 0 (0k, 0M)

--4780--  memcheck: SMs: max_noaccess = 65535 (1048560k, 1023M)

--4780--  memcheck: SMs: max_undefined = 0 (0k, 0M)

--4780--  memcheck: SMs: max_defined = 19 (304k, 0M)

--4780--  memcheck: SMs: max_non_DSМ = 9 (144k, 0M)

--4780--  memcheck: max sec V bit nodes: 0 (0k, 0M)

--4780--  memcheck: set_sec_vbits8 calls: 0 (new: 0, updates: 0)

--4780--  memcheck: max shadow mem size: 448k, 0M

--4780-- translate: fast SP updates identified: 1,456 ( 90.3%)

--4780-- translate: generic_known SP updates identified: 79 ( 4.9%)

--4780-- translate: generic_unknown SP updates identified: 76 ( 4.7%)

--4780--     tt/tc: 3,341 tt lookups requiring 3,360 probes

--4780--     tt/tc: 3,341 fast-cache updates, 3 flushes

--4780--  transtab: new 1,553 (33,037 -> 538,097; ratio 162:10) [0 scs]

--4780--  transtab: dumped 0 (0 -> ??)

--4780--  transtab: discarded 6 (143 -> ??)

--4780-- scheduler: 21,623 jumps (bb entries).

--4780-- scheduler: 0/1,828 major/minor sched events.

--4780--    sanity: 1 cheap, 1 expensive checks.

--4780--    exectx: 30,011 lists, 6 contexts (avq 0 per list)

--4780--    exectx: 6 searches, 0 full compares (0 per 1000)

--4780--    exectx: 0 cmp2, 4 cmp4, 0 cmpAll $

Вы видите, что обнаружены некорректные считывания и записи, и интересующие нас блоки памяти приводятся с указанием места, которое для них отведено. Для прерывания выполнения программы в ошибочном месте можно применить отладчик.

У программы valgrind есть много опций, включая подавление ошибок определенного типа и обнаружение утечки памяти. Для выявления такой утечки в примере вы должны использовать одну из опций, передаваемых valgrind. Для контроля утечек памяти после завершения программы следует задать опцию --leak-check=yes. Список опций можно получить с помощью команды valgrind --help.

Как это работает

Программа выполняется под контролем средства valgrind, которое перехватывает действия, совершаемые программой, и выполняет множество проверок, включая обращения к памяти. Если обращение относится к выделенному блоку памяти и некорректно, valgrind выводит сообщение. В конце программы выполняется подпрограмма "сбора мусора", которая определяет, есть ли выделенные и неосвобожденные блоки памяти. Об этих потерянных блоках выводится сообщение.

Резюме 

В этой главе обсуждались некоторые методы и средства отладки. Система Linux предоставляет ряд мощных инструментов для удаления ошибок из ваших программ. Вы устранили несколько ошибок в программе с помощью отладчика gdb и познакомились с некоторыми средствами статического анализа, такими как cflow и splint. В заключение были рассмотрены проблемы, возникающие при использовании динамически распределяемой памяти, и некоторые средства, способные помочь обнаружить их, например ElectricFence и valgrind.

Утилиты, обсуждавшиеся в этой главе, в основном хранятся на FTP-серверах в Интернете. Авторы, имеющие к ним отношение, могут порой сохранять авторские права на них. Информацию о многих утилитах можно найти в архиве Linux, по адресу http://www.ibiblio.org/pub/Linux. Мы надеемся, что новые версии будут появляться на этом Web-сайте по мере их выхода в свет. 

Глава 11

Процессы и сигналы

Процессы и сигналы формируют главную часть операционной среды Linux. Они управляют почти всеми видами деятельности ОС Linux и UNIX-подобных компьютерных систем. Понимание того, как Linux и UNIX управляют процессами, сослужит добрую службу системным и прикладным программистам или системным администраторам.

В этой главе вы узнаете, как обрабатываются процессы в рабочей среде Linux и как точно установить, что делает компьютер в любой заданный момент времени. Вы также увидите, как запускать и останавливать другие процессы в ваших собственных программах, как заставить процессы отправлять и получать сообщения и как избежать процессов-зомби. В частности, вы узнаете о:

□ структуре процесса, его типе и планировании;

□ разных способах запуска новых процессов;

□ порождающих (родительских), порожденных (дочерних) процессах и процессах-зомби;

□ сигналах и их применении.

Что такое процесс?

Стандарты UNIX, а именно IEEE Std 1003.1, 2004 Edition, определяют процесс как "адресное пространство с одним или несколькими потоками, выполняющимися в нем, и системные ресурсы, необходимые этим потокам. Мы будем рассматривать потоки в главе 12, а пока будем считать процессом просто любую выполняющуюся программу.

Многозадачные системы, такие как Linux, позволяют многим программам выполняться одновременно. Каждый экземпляр выполняющейся программы создает процесс. Это особенно заметно в оконной системе, например Window System (часто называемой просто X). Как и ОС Windows, X предоставляет графический пользовательский интерфейс, позволяющий многим приложениям выполняться одновременно. Каждое приложение может отображаться в одном или нескольких окнах.

Будучи многопользовательской системой, Linux разрешает многим пользователям одновременно обращаться к системе. Каждый пользователь в одно и то же время может запускать много программ или даже несколько экземпляров одной и той же программы. Сама система выполняет в это время другие программы, управляющие системными ресурсами и контролирующие доступ пользователей.

Как вы видели в главе 4, выполняющаяся программа или процесс состоит из программного кода, данных, переменных (занимающих системную память), открытых файлов (файловых дескрипторов) и окружения. Обычно в системе Linux процессы совместно используют код и системные библиотеки, так что в любой момент времени в памяти находится только одна копия программного кода.

Структура процесса

Давайте посмотрим, как организовано сосуществование двух процессов в операционной системе. Если два пользователя neil и rick запускают в одно и то же время программу grep для поиска разных строк в различных файлах, применяемые для этого процессы могут выглядеть так, как показано на рис. 11.1.

Рис. 11.1 

Если вы сможете выполнить команду ps, как в приведенном далее коде, достаточно быстро и до того, как завершатся поиски строк, вывод будет выглядеть подобно следующим строкам:

$ ps -ef

UID  PID PPID С STIME TTY  TIME     CMD

rick 101 96   0 18:24 tty2 00:00:00 grep troi nextgen.doc

neil 102 92   0 18:24 tty4 00:00:00 grep kirk trek.txt

Каждому процессу выделяется уникальный номер, именуемый идентификатором процесса или PID. Обычно это положительное целое в диапазоне от 2 до 32 768. Когда процесс стартует, в последовательности выбирается следующее неиспользованное число. Когда все номера будут исчерпаны, выбор опять начнется с 2. Номер 1 обычно зарезервирован для специального процесса init, который управляет другими процессами. Мы скоро вернемся к процессу init. А пока вы видите, что двум процессам, запущенным пользователями neil и rick, выделены идентификаторы 101 и 102.

Код программы, которая будет выполняться командой grep, хранится в файле на диске. Обычно процесс Linux не может писать в область памяти, применяемую для хранения кода программы, поэтому программный код загружается в память как доступный только для чтения. На рис. 11.1 видно, что несмотря на то, что в данную область нельзя писать, она может безопасно использоваться совместно.

Системные библиотеки также можно совместно использовать. Следовательно, в памяти нужна, например, только одна копия функции printf, даже если многие выполняющиеся программы вызывают ее. Эта схема более сложная, но аналогичная той, которую используют для работы динамически подключаемые библиотеки в ОС Windows.

Как видно из приведенной схемы, дополнительное преимущество заключается в том, что дисковый файл, содержащий исполняемую программу grep, меньше, т.к. не включает программный код совместно используемой библиотеки. Возможно, для одной программы это не слишком ощутимый выигрыш, но извлечение часто используемых подпрограмм, к примеру, из стандартной библиотеки С экономит значительный объем для операционной системы в целом.

Конечно не все, что нужно программе, может быть совместно использовано. Например, переменные отдельно используются каждым процессом. В данном примере искомая строка, передаваемая команде grep, — это переменная s, принадлежащая пространству данных каждого процесса. Эти пространства разделены и, как правило, не могут читаться другим процессом. Файлы, которые применяются в двух командах grep, тоже разные; у каждого процесса есть свой набор файловых дескрипторов, используемых для доступа к файлам.

Кроме того, у каждого процесса есть собственный стек, применяемый для локальных переменных в функциях и для управления вызовами функций и возвратом из них. У процесса также собственное окружение, содержащее переменные окружения, которые могут задаваться только для применения в данном процессе, например, с помощью функций putenv и getenv, как было показано в главе 4. Процесс должен поддерживать собственный счетчик программы, запись того места, до которого он добрался за время выполнения, или поток исполнения. В следующей главе вы увидите, что процессы могут иметь несколько потоков исполнения.

Во многих системах Linux и некоторых системах UNIX существует специальный набор "файлов" в каталоге /proc. Это скорее специальные, чем истинные файлы, т.к. позволяют "заглянуть внутрь" процессов во время их выполнения, как если бы они были файлами в каталогах, В главе 3 мы приводили краткий обзор файловой системы /proc.

И наконец, поскольку Linux, как и UNIX, обладает системой виртуальной памяти, которая удаляет страницы кода и данных на жесткий диск, можно управлять гораздо большим количеством процессов, чем позволяет объем физической памяти.

Таблица процессов

Таблица процессов Linux подобна структуре данных, описывающей все процессы, загруженные в текущий момент, например, их PID, состояние и строку команды, разновидность информационного вывода команды ps. Операционная система управляет процессами с помощью их идентификаторов, PID, которые применяются как указатели в таблице процессов. У таблицы ограниченный размер, поэтому число процессов, поддерживаемых системой, ограничено. В первых системах UNIX оно равнялось 256 процессам. Более современные реализации значительно ослабили это ограничение и ограничены только объемом памяти, доступным для формирования элемента таблицы процессов.

Просмотр процессов

Команда ps показывает выполняемые вами процессы, процессы, выполняемые другим пользователем, или все процессы в системе. Далее приведен еще один пример вывода:

$ ps -ef

UID  PID PPID  С STIME  TTY      TIME CMD

root 433  425  0 18:12  tty1 00:00:00 [bash]

rick 445  426  0 18:12  tty2 00:00:00 -bash

rick 456  427  0 18:12  tty3 00:00:00 [bash]

root 467  433  0 18:12  tty1 00:00:00 sh /usr/X11R6/bin/startx

root 474  467  0 18:12  tty1 00:00:00 xinit /etc/X11/xinit/xinitrc --

root 478  474  0 18:12  tty1 00:00:00 /usr/bin/gnome-session

root 487    1  0 18:12  tty1 00:00:00 gnome-smproxy --sm-client-id def

root 493    1  0 18:12  tty1 00:00:01 [enlightenment]

root 506    1  0 18:12  tty1 00:00:03 panel --sm-client-id defaults

root 508    1  0 18:12  tty1 00:00:00 xscreensaver -no-splash -timeout

root 510    1  0 18:12  tty1 00:00:01 gmc --sm-client-id default10

root 512    1  0 18:12  tty1 00:00:01 gnome-help-browser --sm-client-i

root 649  445  0 18:24  tty2 00:00:00 su

root 653  649  0 18:24  tty2 00:00:00 bash

neil 655  428  0 18:24  tty4 00:00:00 -bash

root 713    1  2 18:27  tty1 00:00:00 gnome-terminal

root 715  713  0 18:28  tty1 00:00:00 gnome-pty-helper

root 717  716 13 18:28 pts/0 00:00:01 emacs

root 718  653  0 18:28  tty2 00:00:00 ps -ef

Вывод отображает информацию о многих процессах, включая процессы, запущенные редактором Emacs в графической среде X ОС Linux. Например, столбец TTY показывает, с какого терминала стартовал процесс, столбец TIME показывает время ЦПУ, затраченное к данному моменту, а столбец CMD — команду, примененную для запуска процесса. Давайте познакомимся поближе с некоторыми из этих процессов.

neil 655  428  0 18:24  tty4 00:00:00 -bash

Начальная регистрация была произведена на консоли номер 4. Это просто консоль на данном компьютере. Выполняемая программа командной оболочки — это стандартная оболочка Linux, bash.

root 467  433  0 18:12  tty1 00:00:00 sh /usr/X11R6/bin/startx

X Window System была запущена командой startx. Это сценарий командной оболочки, который запускает сервер X и выполняет некоторые начальные программы системы X.

root 717  716 13 18:28 pts/0 00:00:01 emacs

Этот процесс представляет окно в системе X, выполняющее программу Emacs. Он был запущен оконным диспетчером в ответ на запрос нового окна. Командной оболочке был назначен новый псевдотерминал pts/0 для считывания и записи.

root 512    1  0 18:12  tty1 00:00:01 gnome-help-browser --sm-client-i

Это обозреватель системы помощи среды GNOME, запущенный оконным диспетчером.

По умолчанию программа ps выводит только процессы, поддерживающие подключение к терминалу, консоли, последовательной линии связи или псевдотерминалу. Другие процессы выполняются без взаимодействия с пользователем на терминале. Обычно это системные процессы, которые система Linux применяет для управления совместно используемыми ресурсами. Команду ps можно применять для отображения всех таких процессов, использовав опцию и запросив "полную" информацию с помощью опции -f.

Примечание

Точная синтаксическая запись команды ps и формат вывода могут немного отличаться в разных системах. Версия GNU команды ps, применяемая в Linux, поддерживает опции, взятые из нескольких предшествующих реализаций ps, включая варианты из UNIX-систем BSD и AT&T, и добавляет множество своих опций. См. интерактивное справочное руководство для получения подробных сведений о доступных опциях и форматах вывода команды ps.

Системные процессы

Далее приведено несколько процессов, выполнявшихся в другой системе Linux. Вывод был сокращен для облегчения понимания. В следующих примерах вы увидите, как определить состояние или статус процесса. Вывод командой ps столбца STAT предоставляет коды текущего состояния процесса. Самые широко распространенные коды перечислены в табл. 11.1. Смысл некоторых из них станет понятен чуть позже в этой главе. Другие же не рассматриваются в данной книге и их можно спокойно игнорировать.

Таблица 11.1

Код STAT Описание
S Спящий. Обычно ждет появления события, такого как сигнал или активизация ввода
R Выполняющийся. Строго говоря "работоспособный", т.е. в очереди на выполнение, либо выполняющийся, либо готовый к выполнению
D Непрерывно спящий (ожидающий). Обычно ждущий завершения ввода или вывода
T Остановленный. Обычно остановленный системой управления заданиями командной оболочки или находящийся под контролем отладчика
Z Умерший или процесс-зомби
N Задача с низким приоритетом, "nice"
W Разбитый на страницы (не используется в Linux с ядром версии 2.6 и последующих версий)
S Ведущий процесс сеанса
+ Процесс в группе фоновых процессов
l Многопотоковый процесс
< Задача с высоким приоритетом

$ ps ах

PID   TTY   STAT TIME COMMAND

1     ?     Ss   0:03 init [5]

2     ?     S    0:00 [migration/0]

3     ?     SN   0:00 [ksoftirqd/0]

4     ?     S<   0:05 [events/0]

5     ?     S<   0:00 [khelper]

6     ?     S<   0:00 [kthread]

840   ?     S<   2:52 [kjournald]

888   ?     S<s  0:03 /sbin/udevd --daemon

3069  ?     Ss   0:00 /sbin/acpid

3098  ?     Ss   0:11 /usr/sbin/hald --daemon=yes

3099  ?     S    0:00 hald-runner

8357  ?     Ss   0:03 /sbin/syslog-ng

8677  ?     Ss   0:00 /opt/kde3/bin/kdm

9119  ?     S    0:11 konsole [kdeinit]

9120  pts/2 Ss   0:00 /bin/bash

9151  ?     Ss   0:00 /usr/sbin/cupsd

9457  ?     Ss   0:00 /usr/sbin/cron

9479  ?     Ss   0:00 /usr/sbin/sshd -o PidFile=/var/run/sshd.init.pid

9618  tty1  Ss+  0:00 /sbin/mingetty --noclear tty1

9619  tty2  Ss+  0:00 /sbin/mingetty tty2

9621  tty3  Ss+  0:00 /sbin/mingetty tty3

9622  tty4  Ss+  0:00 /sbin/mingetty tty4

9623  tty5  Ss+  0:00 /sbin/mingetty tty5

9638  tty6  Ss+  0:00 /sbin/mingetty tty6

10359 tty1  Ss+ 10:05 /usr/bin/Xorg -br -nolisten tcp :0 vt7 -auth

10360 ?     S    0:00 -:0

10381 ?     Ss   0:00 /bin/sh /usr/bin/kde

10438 ?     Ss   0:00 /usr/bin/ssh-agent /bin/bash /etc/X11/xinit/xinitrc

10478 ?     S    0:00 start_kdeinit --new-startup +kcminit_startup

10479 ?     Ss   0:00 kdeinit Running...

10500 ?     S    0:53 kdesktop [kdeinit]

10502 ?     S    1:54 kicker [kdeinit]

10524 ?     Sl   0:47 beagled /usr/lib/beagle/BeagleDaemon.exe --bg

10530 ?     S    0:02 opensuseupdater

10539 ?     S    0:02 kpowersave [kdeinit]

10541 ?     S    0:03 klipper [kdeinit]

10555 ?     S    0:01 kio_uiserver [kdeinit]

10688 ?     S    0:53 konsole [kdeinit]

10689 pts/1 Ss+  0:07 /bin/bash

10784 ?     S    0:00 /opt/kde3/bin/kdesud

11052 ?     S    0:01 [pdflush]

19996 ?     SN1  0:20 beagled-helper /usr/lib/beagle/IndexHelper.exe

20254 ?     S    0:00 qmgr -1 -t fifo -u

21192 ?     Ss   0:00 /usr/sbin/ntpd -p /var/run/ntp/ntpd.pid -u ntp -i /v

21198 ?     S    0:00 pickup -1 -t fifo -u

21475 pts/2 R+   0:00 ps ax

Здесь вы видите на самом деле очень важный процесс

1     ?     Ss   0:03 init [5]

В основном каждый процесс запускается другим процессом, называемым родительским или порождающим процессом. Подобным образом запущенный процесс называют дочерним или порожденным. Когда стартует ОС Linux, она выполняет единственную программу, первого предка и процесс с номером 1, init. Это, если хотите, диспетчер процессов операционной системы и прародитель всех процессов. Другие системные процессы, с которыми вы вскоре встретитесь, запускаются процессом init или другим процессом, запущенным процессом init.

Один из таких примеров — процедура регистрации. Процесс init запускает программу getty для каждого последовательного терминала или модема коммутируемой линии передачи, которые можно применять для регистрации. Эти процессы отображены в следующем выводе команды ps:

9619  tty2  Ss+  0:00 /sbin/mingetty tty2

Процессы getty ждут работы на терминале, приглашая пользователя зарегистрироваться хорошо всем знакомой строкой, и затем передают управление программе регистрации, которая устанавливает окружение пользователя и в конце запускает сеанс командной оболочки. Когда пользовательский сеанс командной оболочки завершается, процесс init запускает новый процесс getty.

Как видите, способность запускать новые процессы и ждать их окончания — одна из основных характеристик системы. Позже в этой главе вы узнаете, как выполнять аналогичные задачи в ваших собственных программах с помощью системных вызовов fork, exec и wait.

Планирование процессов

В следующем примере вывода команды ps приведен элемент списка для самой команды ps.

21475 pts/2 R+   0:00 ps ax

Эта строка означает, что процесс 21475 находится в состоянии выполнения (R) и выполняет он команду ps ах. Таким образом, процесс описан в своем собственном выводе! Индикатор состояния показывает только то, что программа готова к выполнению, а не то, что она обязательно выполняется в данный момент. На однопроцессорном компьютере в каждый момент времени может выполняться только один процесс, в то время как другие процессы ждут своего рабочего периода. Эти периоды, называемые квантами времени, очень короткие и создают впечатление одновременного выполнения программ. Опция R+ просто показывает, что данная программа — фоновая задача, не ждущая завершения других процессов или окончания ввода или вывода данных. Именно поэтому можно увидеть два таких процесса, приведенные в списке вывода команды ps. (Другой, часто встречающийся процесс, помечаемый как выполняющийся, — дисплейный сервер системы X.)

Ядро Linux применяет планировщик процессов для того, чтобы решить, какой процесс получит следующий квант времени. Решение принимается исходя из приоритета процесса (мы обсуждали приоритеты процессов в главе 4). Процессы с высоким приоритетом выполняются чаще, а другие, такие как низкоприоритетные фоновые задачи, — реже. В ОС Linux процессы не могут превысить выделенный им квант времени. Они преимущественно относятся к разным задачам, поэтому приостанавливаются и возобновляются без взаимодействия друг с другом. В более старых системах, например Windows 3.х, как правило, для возобновления других процессов требовалось явное согласие процесса.

В многозадачных системах, таких как Linux, несколько программ могут претендовать на один и тот же ресурс, поэтому программы с короткими рабочими циклами, прерывающиеся для ввода, считаются лучше ведущими себя, чем программы, прибирающие к рукам процессор для продолжительного вычисления какого-либо значения или непрерывных запросов к системе, касающихся готовности ввода данных. Хорошо ведущие себя программы называют nice-программами (привлекательными программами) и в известном смысле эту "привлекательность" можно измерить. Операционная система определяет приоритет процесса на основе значения "nice", по умолчанию равного 0, и поведения программы. Программы, выполняющиеся без пауз в течение долгих периодов, как правило, получают более низкие приоритеты. Программы, делающие паузы время от времени, например в ожидании ввода, получают награду. Это помогает сохранить отзывчивость программы, взаимодействующей с пользователем; пока она ждет какого-либо ввода от пользователя, система увеличивает ее приоритет, чтобы, когда программа будет готова возобновить выполнение, у нее был высокий приоритет. Задать значение nice для процесса можно с помощью команды nice, а изменить его — с помощью команды renice. Команда nice увеличивает на 10 значение nice процесса, присваивая ему более низкий приоритет. Просмотреть значения nice активных процессов можно с помощью опций -l или -f (для полного вывода) команды ps. Интересующие вас значения представлены в столбце NI (nice).

$ ps -l

  F S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

000 S 500 1259 1254 0  75  0 -   710 wait4  pts/2 00:00:00 bash

000 S 500 1262 1251 0  75  0 -   714 wait4  pts/1 00:00:00 bash

000 S 500 1313 1262 0  75  0 -  2762 schedu pts/1 00:00:00 emacs

000 S 500 1362 1262 2  80  0 -   789 schedu pts/1 00:00:00 oclook

000 R 500 1363 1262 0  81  0 -   782 -      pts/1 00:00:00 ps

Как видно из списка, программа oclock выполняется (как процесс 1362) со значением nice по умолчанию. Если бы она была запущена командой

$ nice oclock &

то получила бы значение nice +10. Если вы откорректируете это значение командой

$ renice 10 1362

1362: old priority 0, new priority 10

программа oclock будет выполняться реже. Увидеть измененное значение nice можно снова с помощью команды ps:

$ ps -l

F   S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

000 S 500 1259 1254 0  75  0 -   710 wait4  pts/2 00:00:00 bash

000 S 500 1262 1251 0  75  0 -   714 wait4  pts/1 00:00:00 bash

000 S 500 1313 1262 0  75  0 -  2762 schedu pts/1 00:00:00 emacs

000 S 500 1362 1262 0  90 10 -   789 schedu pts/1 00:00:00 oclock

000 R 500 1365 1262 0  81  0 -   782 -      pts/1 00:00:00 ps

Столбец состояния теперь также содержит N, указывая на то, что значение nice было изменено по сравнению с принятым по умолчанию:

ps х

PID  TTY   STAT TIME COMMAND

1362 pts/1 SN   0:00 oclock

Поле PPID в выводе команды ps содержит ID родительского процесса (PID), либо процесса, запустившего данный процесс, либо, если этот процесс уже не выполняется, процесса init (PID, равный 1).

Планировщик процессов ОС Linux решает, какому процессу разрешить выполнение, на основе приоритета. Конкретные реализации конечно отличаются, но высокоприоритетные процессы выполняются чаще. В некоторых случаях низкоприоритетные процессы не выполняются совсем, если высокоприоритетные процессы готовы к выполнению.

Запуск новых процессов

Применив библиотечную функцию system, вы можете заставить программу выполняться из другой программы и тем самым создать новый процесс:

#include <stdlib.h>

int system(const char *string);

Функция system выполняет команду, переданную ей как строку, и ждет ее завершения. Команда выполняется, как если бы командной оболочке была передана следующая команда:

$ sh -с string

Функция system возвращает код 127, если командная оболочка не может быть запущена для выполнения команды, и -1 в случае другой ошибки. Иначе system вернет код завершения команды.

Выполните упражнение 11.1.

Упражнение 11.1. Функция system

Вы можете использовать system для написания программы, выполняющей команду ps. Хотя нельзя сказать, что она необычайно полезна, вы увидите, как применять этот метод в последующих примерах. (Для простоты примера мы не проверяем, работает ли на самом деле системный вызов.)

#include <stdlib.h>

#include <stdio.h>

int main() {

 printf("Running ps with system\n");

 system("ps ax");

 printf("Done \n");

 exit(0);

}

Когда вы откомпилируете и выполните программу system1.с, то получите вывод, похожий на приведенный далее:

$ ./system1

Running ps with system

 PID TTY   STAT TIME COMMAND

   1 ?     Ss   0:03 init [5]

...

1262 pts/1 Ss   0:00 /bin/bash

1273 pts/2 S    0:00 su -

1274 pts/2 S+   0:00 -bash

1463 pts/2 SN   0:00 oclock

1465 pts/1 S    0:01 emacs Makefile

1480 pts/1 S+   0:00 ./system1

1481 pts/1 R+    0:00 ps ax

Done.

Поскольку функция system применяет командную оболочку для запуска нужной программы, вы можете перевести ее в фоновый режим, заменив вызов функции в файле system1.с на следующий:

system("ps ах &");

Когда вы откомпилируете и выполните эту версию программы, то получите следующий вывод:

$ ./system2

Running ps with system

 PID TTY  STAT TIME COMMAND

   1 ?    S    0:03 init [5]

 ...

Done.

$ 1274 pts/2 3+ 0:00 -bash

1463 pts/2 SN  0:00 oclock

1465 pts/1 S   0:01 emacs Makefile

1484 pts/1 R   0:00 ps ax

Как это работает

В первом примере программа вызывает функцию system со строкой "ps ах", выполняющую программу ps. Когда команда ps завершается, вызов system возвращает управление программе. Функция system может быть очень полезной, но она тоже ограничена. Поскольку программа вынуждена ждать, пока не завершится процесс, начатый вызовом system, вы не можете продолжить выполнение других задач.

Во втором примере вызов функции system вернет управление программе, как только завершится команда командной оболочки. Поскольку это запрос на выполнение программы в фоновом режиме, командная оболочка вернет управление в программу, как только будет запущена программа ps, ровно то же, что произошло бы при вводе в строку приглашения командной оболочки команды

$ ps ах &

Далее программа system2 выводит Done. и завершается до того, как у команды ps появится возможность отобразить до конца весь свой вывод. Вывод ps продолжает формироваться после завершения system2 и в этом случае не включает в список элемент, описывающий процесс system2. Такое поведение процесса может сильно сбить с толку пользователей. Для того чтобы умело применять процессы, вы должны лучше управлять их действиями. Давайте рассмотрим низкоуровневый интерфейс для создания процесса, exec.

Примечание

Вообще применение функции system — далеко не идеальный способ создания процессов, потому что запускаемая программа использует командную оболочку. Он неэффективен вдвойне: и потому что перед запуском программы запускается оболочка, и потому что сильно зависим от варианта установки командной оболочки и применяемого окружения. В следующем разделе вы увидите гораздо более удачный способ запуска программ, который почти всегда предпочтительней применения вызова system.

Замена образа процесса

Существует целое семейство родственных функций, сгруппированных под заголовком exec. Они отличаются способом запуска процессов и представлением аргументов программы. Функция exec замещает текущий процесс новым, заданным в аргументе path или file. Функции exec можно применять для передачи выполнения вашей программы другой программе. Например, перед запуском другого приложения с политикой ограниченного применения вы можете проверить имя пользователя и пароль. Функции exec более эффективны по сравнению с system, т.к. исходная программа больше не будет выполняться после запуска новой программы.

#include <unistd.h>

char **environ;

int execl(const char *path, const char *arg0, ..., (char *)0);

int execlp(const char *file, const char *arg0, ..., (char *)0);

int execle(const char *path, const char *arg0, ..., (char *)0,

 char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);

Эти функции делятся на два вида. execl, execlp и execle принимают переменное число аргументов, заканчивающихся указателем null. У execv и execvp второй аргумент — массив строк. В обоих случаях новая программа стартует с заданными аргументами, представленными в массиве argv, передаваемом функции main.

Эти функции реализованы, как правило, с использованием execve, хотя нет обязательных требований на этот счет.

Функции, имена которых содержат суффикс p, отличаются тем, что ищут переменную окружения PATH для определения исполняемого файла новой программы. Если эта переменная не позволяет найти нужный файл, необходимо передать функции как параметр абсолютное имя файла, включающее каталоги.

Передать значение окружению программы может глобальная переменная environ. Другой вариант — дополнительный аргумент в функциях execle и execve, способный передавать строки, используемые как окружение новой программы.

Если вы хотите применить функцию exec для запуска программы ps, можно выбирать любую функцию из семейства exec, как показано в вызовах приведенного далее фрагмента программного кода:

#include <unistd.h>

/* Пример списка аргументов */

/* Учтите, что для argv[0] необходимо имя программы */

char *const ps_argv[] = {"ps", "ax", 0};

/* He слишком полезный пример окружения */

char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};

/* Возможные вызовы функций exec */

execl("/bin/ps", "ps", "ax", 0);

/* предполагается, что ps в /bin */

execlp("ps", "ps", "ax", 0);

/* предполагается, что /bin в PATH */

execle("/bin/ps", "ps", "ax", 0, ps_envp);

/* передается свое окружение */

execv("/bin/ps", ps_argv);

execvp("ps", ps_argv);

execve("/bin/ps", ps_argv, ps_envp);

А теперь выполните упражнение 11.2.

Упражнение 11.2. Функция execlp

Давайте изменим пример и используем вызов execlp:

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 printf("Running ps with execlp\n");

 execlp("ps", "ps", "ax", 0);

 printf("Done.\n");

 exit(0);

}

Когда вы выполните эту программу, рехес.с, то получите обычный вывод команды ps, но без сообщения Done. Кроме того, обратите внимание на то, что в выводе нет процесса с именем рехес:

$ ./рехес

Running ps with execlp

 PID TTY   STAT TIME COMMAND

1    ?     S    0:03 init [5]

...

1262 pts/1 Ss   0:00 /bin/bash

1273 pts/2 S    0:00 su -

1274 pts/2 S+   0:00 -bash

1463 pts/1 SN   0:00 oclock

1465 pts/1 S    0:01 emacs Makefile

1514 pts/1 R+   0:00 ps ax

Как это работает

Программа выводит первое сообщение и затем вызывает функцию execlp, которая ищет каталоги, заданные в переменной окружения PATH для обнаружения программы ps. Далее она выполняет команду вместо программы рехес, запустив ее так, как будто вы ввели команду командной оболочки

$ ps ax

Когда ps завершается, вы получаете новую строку приглашения командной оболочки. Возврата в программу рехес не происходит, поэтому второе сообщение не выводится. PID нового процесса тот же, что и у исходного, то же самое можно сказать о PID родительского процесса и значении nice. В сущности, происходит следующее: выполняющаяся программа запустила на выполнение новый код и новый исполняемый файл, заданный в вызове функции exec.

Существует ограничение для общего размера списка аргументов и окружения процесса, запускаемого функциями exec. Оно задается в переменной ARG_MAX и в системах Linux равно 128 Кбайт. В других системах может задаваться меньший предельный размер, что способно порождать проблемы. Стандарт POSIX гласит, что ARG_MAX должна быть не менее 4096 байтов.

Функции exec, как правило, не возвращаются в программу до тех пор, пока не возникла ошибка, в этом случае задается переменная errno и функция exec возвращает -1.

Новые процессы, запущенные exec, наследуют многие свойства исходного процесса. В частности, открытые файловые дескрипторы остаются открытыми в новом процессе, пока не установлен их флаг FD_CLOEXEC (close on exec) (подробную информацию см. в описании системного вызова fcntl в главе 3). Любые открытые в исходном процессе потоки каталогов закрываются.

Дублирование образа процесса

Для применения процессов, выполняющих несколько функций одновременно, можно либо использовать потоки, обсуждаемые в главе 12, либо создавать в программе полностью отдельный процесс, как делает init, вместо замещения текущего потока исполнения, как в случае применения функции exec.

Создать новый процесс можно с помощью вызова fork. Системный вызов дублирует текущий процесс, создавая новый элемент в таблице процессов с множеством атрибутов, таких же как у текущего процесса. Новый процесс почти идентичен исходному, выполняет тот же программный код, но в своем пространстве данных, окружении и со своими файловыми дескрипторами. В комбинации с функциями exec вызов fork — все, что вам нужно для создания новых процессов.

#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);

Как видно из рис. 11.2, вызов fork возвращает в родительский процесс PID нового дочернего процесса. Новый процесс продолжает выполнение так же, как и исходный, за исключением того, что в дочерний процесс вызов fork возвращает 0. Это позволяет родительскому и дочернему процессам определить, "кто есть кто".

Рис. 11.2 

Если вызов fork завершается аварийно, он возвращает -1. Обычно это происходит из-за ограничения числа дочерних процессов, которые может иметь родительский процесс (CHILD_MAX), в этом случае переменной errno будет присвоено значение EAGAIN. Если для элемента таблицы процессов недостаточно места или не хватает виртуальной памяти, переменная errno получит значение ENOMEM.

Далее приведен фрагмент типичного программного кода, использующего вызов fork:

pid_t new_pid;

new_pid = fork();

switch(new_pid) {

case -1:

 /* Ошибка */

 break;

case 0:

 /* Мы — дочерний процесс */

 break;

default:

 /* Мы — родительский процесс */

 break;

}

Выполните упражнение 11.3.

Упражнение 11.3. Системный вызов fork

Давайте рассмотрим простой пример fork1.с:

#include <sys/types.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 pid_t pid;

 char* message;

 int n;

 printf("fork program starting\n");

 pid = fork();

 switch(pid) {

 case -1:

  perror("fork failed");

  exit(1);

 case 0:

  message = "This is the child";

  n = 5;

  break;

 default:

  message = "This is the parent";

  n = 3;

  break;

 }

 for (; n > 0; n--) {

  puts(message);

  sleep(1);

 }

 exit(0);

}

Эта программа выполняет два процесса. Дочерний процесс создается и выводит пять раз сообщение. Исходный процесс (родитель) выводит сообщение только три раза. Родительский процесс завершается до того, как дочерний процесс выведет все свои сообщения, поэтому в вывод попадает очередное приглашение командной оболочки.

$ ./fork1

fork program starting

This is the child

This is the parent

This is the parent

This is the child

This is the parent

This is the child

$ This is the child

This is the child

Как это работает

Когда вызывается fork, эта программа делится на два отдельных процесса. Родительский процесс идентифицируется ненулевым возвращаемым из fork значением и используется для задания количества сообщений, выводимых с интервалом в одну секунду.

Ожидание процесса

Когда вы запускаете дочерний процесс с помощью вызова fork, он начинает жить собственной жизнью и выполняется независимо. Иногда вам нужно знать, когда закончился дочерний процесс. Например, в предыдущей программе родительский процесс завершается раньше дочернего, и вы получаете слегка беспорядочный вывод, потому что дочерний процесс продолжает выполняться. Вы можете с помощью системного вызова wait заставить родительский процесс дождаться завершения дочернего процесса перед своим продолжением.

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *stat_loc);

Системный вызов wait заставляет родительский процесс сделать паузу до тех пор, пока один из его дочерних процессов не остановится. Вызов возвращает PID дочернего процесса. Обычно это дочерний процесс, который завершился. Сведения о состоянии позволяют родительскому процессу определить статус завершения дочернего процесса, т.е. значение, возвращенное из функции main или переданное функции exit. Если stat_loc не равен пустому указателю, информация о состоянии будет записана в то место, на которое указывает этот параметр.

Интерпретировать информацию о состоянии процесса можно с помощью макросов, описанных в файле sys/wait.h и приведенных в табл. 11.2.

Таблица 11.2

Макрос Описание
WIFEXITED(stat_val) Ненулевой, если дочерний процесс завершен нормально
WEXITSTATUS(stat_val) Если WIFEXITED ненулевой, возвращает код завершения дочернего процесса
WIFSIGNALED(stat_val) Ненулевой, если дочерний процесс завершается неперехватываемым сигналом
WTERMSIG(stat_val) Если WIFSIGNALED ненулевой, возвращает номер сигнала
WIFSTOPPED(stat_val) Ненулевой, если дочерний процесс остановился
WSTOPSIG(stat_val) Если WIFSTOPPED ненулевой, возвращает номер сигнала

Выполните упражнение 11.4.

Упражнение 11.4. Системный вызов wait

В этом упражнении вы слегка измените программу, чтобы можно было подождать и проверить код состояния дочернего процесса. Назовите новую программу wait.c.

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main() {

 pid_t pid;

 char* message;

 int n;

 int exit_code;

 printf("fork program starting\n");

 pid = fork();

 switch(pid) {

 case -1:

  perror("fork failed");

  exit(1);

 case 0:

  message = "This is the child";

  n = 5;

  exit_code = 37;

  break;

 default:

  message = "This is the parent";

  n = 3;

  exit_code = 0;

  break;

 }

 for (; n > 0; n--) {

  puts(message);

  sleep(1);

 }

Следующий фрагмент программы ждет окончания дочернего процесса:

 if (pid != 0) {

  int stat_val;

  pid_t child_pid;

  child_pid = wait(&stat_val);

  printf("Child has finished: PID = %d\n", child_pid);

  if (WIFEXITED(stat_val))

   printf("Child exited with code %d\n", WEXITSTATUS(stat_val));

  else printf("Child terminated abnormally\n");

 }

 exit(exit_code);

}

Когда вы выполните эту программу, то увидите, что родительский процесс ждет дочерний:

$ ./wait

fork program starting

This is the child

This is the parent

This is the parent

This is the child

This is the parent

This is the child

This is the child

This is the child

Child has finished: PID = 1582

Child exited with code 37

$

Как это работает

Родительский процесс, получивший ненулевое значение, возвращенное из вызова fork, применяет системный вызов wait для приостановки своего выполнения до тех пор, пока информация о состоянии дочернего процесса не станет доступной. Это произойдет, когда дочерний процесс вызовет функцию exit; мы присвоили ему код завершения 37. Далее родительский процесс продолжается, определяет, протестировав значение, возвращенное вызовом wait, что дочерний процесс завершился нормально, и извлекает код завершения из информации о состоянии процесса.

Процессы-зомби

Применение вызова fork для создания процессов может оказаться очень полезным, но вы должны отслеживать дочерние процессы. Когда дочерний процесс завершается, связь его с родителем сохраняется до тех пор, пока родительский процесс в свою очередь не завершится нормально, или не вызовет wait. Следовательно, запись о дочернем процессе не исчезает из таблицы процессов немедленно. Становясь неактивным, дочерний процесс все еще остается в системе, поскольку его код завершения должен быть сохранен, на случай если родительский процесс в дальнейшем вызовет wait. Он становится умершим или процессом-зомби.

Вы сможете увидеть создание процесса-зомби, если измените количество сообщений в программе из примера с вызовом fork. Если дочерний процесс выводит меньше сообщений, чем родительский, он закончится первым и будет существовать как зомби, пока не завершится родительский процесс.

Упражнение 11.5. Зомби

Программа fork2.c такая же, как программа fork1.с, за исключением того, что количества сообщений, выводимых родительским и дочерним процессами, поменяли местами. Далее приведены соответствующие строки кода:

switch (pid) {

case -1:

 perror("fork failed");

 exit(1);

case 0:

 message = "This is the child";

 n = 3;

 break;

default:

 message = "This is the parent";

 n = 5;

 break;

}

Как это работает

Если вы выполните только что приведенную программу с помощью команды ./fork2 & и затем вызовите программу ps после завершения дочернего процесса, но до окончания родительского, то увидите строку, подобную следующей. (Некоторые системы могут сказать <zombie> вместо <defunct>.)

$ ps -аl

  F S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

004 S   0 1273 1259 0  75  0 -   589 wait4  pts/2 00:00:00 su

000 S   0 1274 1273 0  75  0 -   731 schedu pts/2 00:00:00 bash

000 S 500 1463 1262 0  75  0 -   788 schedu pts/1 00:00:00 oclock

000 S 500 1465 1262 0  75  0 -  2569 schedu pts/1 00:00:01 emacs

000 S 500 1603 1262 0  75  0 -   313 schedu pts/1 00:00:00 fork2

003 Z 500 1604 1603 0  75  0 -     0 do_exi pts/1 00:00:00 fork2 <defunct>

000 R 500 1605 1262 0  81  0 -   781 -      pts/1 00:00:00 ps

Если родительский процесс завершится необычно, дочерний процесс автоматически получит в качестве родителя процесс с PID, равным 1 (init). Теперь дочерний процесс — зомби, который уже не выполняется, но унаследован процессом init из-за необычного окончания родительского процесса. Зомби останется в таблице процессов, пока не пойман процессом init. Чем больше таблица, тем медленнее эта процедура. Следует избегать процессов-зомби, поскольку они потребляют ресурсы до тех пор, пока процесс init не вычистит их.

Есть еще один системный вызов, который можно применять для ожидания дочернего процесса. Он называется waitpid и применяется для ожидания завершения определенного процесса.

#include <sys/types.h>

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *stat_loc, int options);

Аргумент pid — конкретный дочерний процесс, окончания которого нужно ждать. Если он равен –1, waitpid вернет информацию о любом дочернем процессе. Как и вызов wait, он записывает информацию о состоянии процесса в место, указанное аргументом stat_loc, если последний не равен пустому указателю. Аргумент options позволяет изменить поведение waitpid. Наиболее полезная опция WNOHANG мешает вызову waitpid приостанавливать выполнение вызвавшего его процесса. Ее можно применять для выяснения, завершился ли какой-либо из дочерних процессов, и если нет, то продолжать выполнение. Остальные опции такие же, как в вызове wait.

Итак, если вы хотите, чтобы родительский процесс периодически проверял, завершился ли конкретный дочерний процесс, можно использовать следующий вызов:

waitpid(child_pid, (int *)0, WNOHANG);

Он вернет ноль, если дочерний процесс не завершился и не остановлен, или child_pid, если это произошло. Вызов waitpid вернет -1 в случае ошибки и установит переменную errno. Это может произойти, если нет дочерних процессов (errno равна ECHILD), если вызов прерван сигналом (EINTR) или аргумент options неверный (EINVAL).

Перенаправление ввода и вывода

Вы можете применить ваши знания о процессах для изменения поведения программ, используя тот факт, что открытые файловые дескрипторы сохраняются вызовами fork и exec. Следующий пример из упражнения 11.6 содержит программу-фильтр, которая читает из стандартного ввода и пишет в свой стандартный вывод, выполняя при этом некоторое полезное преобразование.

Далее приведена программа очень простой фильтрации upper.c, которая читает ввод и преобразует строчные буквы в прописные:

#include <stdio.h>

#include <ctype.h>

#include <stdlib.h>

int main() {

 int ch;

 while ((ch = getchar()) != EOF) {

  putchar(toupper(ch));

 }

 exit(0);

}

Когда вы выполните программу, она сделает то, что и ожидалось:

$ ./upper

hello THERE

HELLO THERE

^D

$

Вы, конечно, можете применить ее для преобразования символов файла, используя перенаправление, применяемое командной оболочкой:

$ cat file.txt

this is the file, file.txt, it is all lower case.

$ ./upper < file.txt

THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

Что если вы хотите применить этот фильтр из другой программы? Программа useupper.c принимает имя файла как аргумент и откликается сообщением об ошибке при некорректном вызове:

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char *argv[]) {

 char *filename;

 if (argc != 2) {

  fprintf (stderr, "usage: useupper file\n");

  exit(1);

 }

 filename = argv[1];

Вы повторно открываете стандартный ввод, снова при этом проверяете наличие любых ошибок, а затем применяете функцию execl для вызова программы upper:

 if (!freopen(filename, "r", stdin)) {

  fprintf(stderr, "could not redirect stdin from file %s\n", filename);

  exit(2);

 }

 execl("./upper", "upper", 0);

He забудьте, что execl заменяет текущий процесс, если ошибок нет, оставшиеся строки не выполняются.

 perror("could not exec ./upper");

 exit(3);

}

Как это работает

Когда вы выполняете эту программу, ей можно передать файл для преобразования в прописные буквы. Работа делается программой upper, которая не обрабатывает аргументы с именами файлов. Обратите внимание на то, что вам не нужен исходный код программы upper; таким способом можно запустить любую исполняемую программу.

$ ./useupper file.txt

THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

Программа useupper применяет freopen для закрытия стандартного ввода и связывания потока файла с файлом, заданным как аргумент программы. Затем она вызывает execl, чтобы заменить код выполняемого процесса кодом программы upper. Поскольку файловые дескрипторы сохраняются, пройдя сквозь вызов execl, программа upper выполняется так же, как при вводе ее в строке командной оболочки

$ ./upper < file.txt

Потоки 

Процессы Linux могут взаимодействовать, отправлять друг другу сообщения и прерываться друг другом. Они могут даже организоваться и совместно использовать сегменты памяти, но они остаются обособленными объектами операционной системы. Процессы не настроены на совместное использование переменных.

Существует класс процесса, именуемый потоком (thread), который доступен во многих системах UNIX и Linux. Несмотря на то, что потоки трудно, программировать, они могут быть очень важны для некоторых приложений, таких как многопоточные серверы баз данных. Программирование потоков в Linux (и вообще в UNIX) не так распространено, как применение множественных процессов, поскольку процессы Linux очень легко применять и программирование множественных взаимодействующих процессов гораздо легче программирования потоков. Потоки обсуждаются в главе 12.

Сигналы

Сигнал — это событие, генерируемое системами UNIX и Linux в ответ на некоторую ситуацию, получив сообщение о котором процесс, в свою очередь, может предпринять какое-то действие. Мы применяем термин "возбуждать" (raise) для обозначения генерации сигнала и термин "захватывать" (catch) для обозначения получения или приема сигнала. Сигналы возбуждаются некоторыми ошибочными ситуациями, например нарушениями сегментации памяти, ошибками процессора при выполнении операций с плавающей точкой или некорректными командами. Они генерируются командной оболочкой и обработчиками терминалов для вызова прерываний и могут явно пересылаться от одного процесса к другому как способ передачи информации или коррекции поведения. Во всех этих случаях программный интерфейс один и тот же. Сигналы могут возбуждаться, улавливаться и соответственно обрабатываться или (по крайней мере, некоторые) игнорироваться.

Имена сигналов задаются с помощью включенного заголовочного файла signal.h. Они начинаются с префикса SIG и включают приведенные в табл. 11.3 сигналы.

Таблица 11.3

Имя сигнала Описание
SIGABORT *Процесс аварийно завершается
SIGALRM Сигнал тревоги
SIGFPE *Исключение операции с плавающей точкой
SIGHUP Неожиданный останов или разъединение
SIGILL *Некорректная команда
SIGINT Прерывание терминала
SIGKILL Уничтожение (не может быть перехвачен или игнорирован)
SIGPIPE Запись в канал без считывателя
SIGQUIT Завершение работы терминала
SIGSEGV *Некорректный доступ к сегменту памяти
SIGTERM Завершение, выход
SIGUSR1 Сигнал 1, определенный пользователем
SIGUSR2 Сигнал 2, определенный пользователем

*Могут быть также предприняты действия, зависящие от конкретной реализации.

Если процесс получает один из этих сигналов без предварительной подготовки к его перехвату, процесс будет немедленно завершен. Обычно при этом создается файл с дампом ядра. Этот файл в текущем каталоге, названный core, представляет собой образ процесса, который может оказаться полезным при отладке.

К дополнительным относятся сигналы, приведенные в табл. 11.4.

Таблица 11.4

Имя сигнала Описание
SIGCHLD Дочерний процесс остановлен или завершился
SIGCONT Продолжить выполнение, если процесс был приостановлен
SIGSTOP Остановить выполнение (не может захватываться или игнорироваться)
SIGTSTP Сигнал останова, посылаемый с терминала
SIGTTIN Фоновый процесс пытается читать
SIGTTOU Фоновый процесс пытается писать

Сигнал SIGCHLD может быть полезен для управления дочерними процессами. По умолчанию он игнорируется. Остальные сигналы заставляют процессы, получившие их, остановиться, за исключением сигнала SIGCONT, который вызывает возобновление процесса. Они применяются программами командной оболочки для контроля работы и редко используются в пользовательских программах.

Чуть позже мы рассмотрим более подробно первую группу сигналов. Пока же достаточно знать, что если командная оболочка и драйвер терминала нормально настроены, ввод символа прерывания (обычно от нажатия комбинации клавиш <Ctrl>+<C>) с клавиатуры приведет к отправке сигнала SIGINT приоритетному процессу, т.е. программе, выполняющейся в данный момент. Это вызовет завершение программы, если в ней не предусмотрен перехват сигнала,

Если вы хотите отправить сигнал не текущей приоритетной задаче, а другому процессу, используйте команду kill. Она принимает для отправки процессу в качестве необязательного параметра имя сигнала или его номер и PID (который, как правило, можно определить с помощью команды ps). Например, для отправки сигнала "останов или разъединение" командной оболочке, выполняющейся на другом терминале с PID 512, вы должны применить следующую команду:

$ kill -HUP 512

Удобный вариант команды kill — команда killall, которая позволяет отправить сигнал всем процессам, выполняющим конкретную команду. Не все системы UNIX поддерживают ее, но ОС Linux, как правило, поддерживает. Этот вариант полезен, когда вы не знаете PID процесса или хотите отправить сигнал нескольким разным процессам, выполняющим одну и ту же команду. Обычное применение — заставить программу inetd перечитать параметры настройки. Для этого можно воспользоваться следующей командой:

$ killall -HUP inetd

Программы могут обрабатывать сигналы с помощью библиотечной функции signal.

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

Это довольно сложное объявление говорит о том, что signal — это функция, принимающая два параметра, sig и func. Сигнал, который нужно перехватить или игнорировать, задается аргументом sig. Функция, которую следует вызвать при получении заданного сигнала, содержится в аргументе func. Эта функция должна принимать единственный аргумент типа int (принятый сигнал) и иметь тип void. Функция сигнала возвращает функцию того же типа, которая является предыдущим значением функции, заданной для обработки сигнала, или одно из двух специальных значений:

SIG_IGN — игнорировать сигнал;

SIG_DFL — восстановить поведение по умолчанию.

Пример сделает все понятным. В упражнении 11.7 вы напишете программу ctrlc.c, которая реагирует на нажатие комбинации клавиш <Ctrl>+<C> вместо обычного завершения выводом соответствующего сообщения. Повторное нажатие <Ctrl>+<C> завершает программу.

Упражнение 11.7. Обработка сигнала

Функция ouch реагирует на сигнал, передаваемый в параметре sig. Эта функция будет вызываться, когда возникнет сигнал. Она выводит сообщение и затем восстанавливает обработку сигнала по умолчанию для сигнала SIGINT (генерируется при нажатии комбинации клавиш <Ctrl>+<C>).

#include <signal.h>

#include <stdio.h>

#include <unistd.h>

void ouch(int sig) {

 printf("OUCH! - I got signal %d\n", sig);

 (void)signal(SIGINT, SIG_DFL);

}

Функция main должна взаимодействовать с сигналом SIGINT, генерируемым при нажатии комбинации клавиш <Ctrl>+<C>. В остальное время она находится в бесконечном цикле, выводя один раз в секунду сообщение.

int main() {

 (void)signal(SIGINT, ouch);

 while(1) {

  printf("Hello World!\n");

  sleep(1);

 }

}

Ввод комбинации клавиш <Ctrl>+<C> (отображается как ^C в следующем далее выводе) в первый раз заставляет программу отреагировать и продолжиться. Когда вы нажимаете <Ctrl>+<C> снова, программа завершается, т.к. сигнал SIGINT вернул программе стандартное поведение, заставляющее ее завершиться.

$ ./ctrlcl

Hello World!

Hello World!

Hello World!

Hello World!

^C

OUCH! - I got signal 2

Hello World!

Hello World!

Hello World!

Hello World!

^C

$

Как видно из данного примера, функция обработки сигнала принимает один целочисленный параметр — номер сигнала, приводящий к вызову функции. Это удобно, если одна и та же функция применяется для обработки нескольких сигналов. В данном случае вы выводите значение SIGINT, которое в этой системе оказывается равным 2. Не стоит полагаться на стандартные числовые значения сигналов, в новых программах всегда пользуйтесь именами сигналов.

Примечание

Вызывать из обработчика сигнала все функции, например, printf, небезопасно. Удобный метод — использовать флаг, устанавливаемый в обработчике сигнала, и затем проверять этот флаг в функции main и выводить сообщение, если нужно. В конце этой главы вы найдете список вызовов, которые можно безопасно применять в теле обработчиков сигналов.

Как это работает

Программа устроена так, что, когда вы задаете сигнал SIGINT, нажимая комбинацию клавиш <Ctrl>+<C>, вызывает функцию ouch. После того как функция прерывания ouch завершится, программа продолжает выполняться, но восстанавливает реакцию на сигнал, принятую по умолчанию. (У разных версий UNIX, в особенности у потомков системы Berkeley UNIX, в течение многих лет сложилось разное поведение при получении сигналов. Если вы хотите восстановить поведение по умолчанию после возникновения сигнала, лучше всего запрограммировать его на конкретные действия.) Когда программа получает второй сигнал SIGINT, она выполняет стандартное действие, приводящее к завершению программы.

Если вы хотите сохранить обработчик сигнала и продолжать реагировать на комбинацию клавиш <Ctrl>+<C>, вам придется восстановить его, вызвав функцию signal еще раз. Это приведет к возникновению короткого промежутка времени, начиная с запуска функции прерывания и до момента восстановления обработчика сигнала, в течение которого сигнал не будет обрабатываться. Если второй сигнал будет получен в этот период, вопреки вашим желаниям программа может завершиться.

Примечание

Мы не рекомендуем вам пользоваться функцией signal для перехвата сигналов. Мы включили ее в книгу, потому что она будет часто встречаться в более старых программах. Позже вы увидите sigaction, более четко определенный и надежный интерфейс, который следует применять в новых программах.

Функция signal возвращает предыдущее значение обработчика для заданного типа сигнала, если таковой есть, или в противном случае SIG_ERR с установкой положительного значения в переменной errno. Если задан неверный сигнал или делается попытка обработать сигнал, который не может быть перехвачен или игнорироваться, например SIGKILL, переменной errno присваивается значение EINVAL.

Отправка сигналов

Процесс может отправить сигнал другому процессу, включая себя самого, с помощью вызова функции kill. Вызов завершится аварийно, если у программы нет полномочий на отправку сигнала, часто потому что процесс-получатель принадлежит другому пользователю. Эта функция эквивалентна команде оболочки с тем же именем.

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

Функция kill посылает заданный сигнал sig процессу с идентификатором, заданным в аргументе pid. В случае успеха она возвращает 0. Для отправки сигнала посылающий процесс должен иметь право на выполнение этого действия. Обычно это означает, что у обоих процессов должен быть один и тот же идентификатор пользователя ID (т.е. вы можете отправить сигнал только одному из собственных процессов, хотя суперпользователь может отправлять сигналы любому процессу).

Функция kill завершится аварийно, вернет -1 и установит значение переменной errno, если задан неверный сигнал, (errno равна EINVAL), у процесса нет полномочий (EPERM) или заданный процесс не существует (ESRCH).

Сигналы предоставляют полезное средство, именуемое будильником или сигналом тревоги. Вызов функции alarm может применяться для формирования сигнала SIGALRM в определенное время в будущем.

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

Вызов alarm намечает доставку сигнала SIGALRM через seconds секунд. В действительности сигнал будильника будет доставлен чуть позже из-за обработки задержек и учета неопределенностей. Значение 0 отменяет любой невыполненный запрос на сигнал будильника. Вызов функции alarm до получения сигнала может вызвать сброс графика доставки. У каждого процесса может быть только один невыполненный сигнал будильника. Функция alarm возвращает количество секунд, оставшихся до отправки любого невыполненного вызова, alarm, или -1 в случае аварийного завершения.

Для того чтобы увидеть как работает функция alarm, можно сымитировать ее действие, используя вызовы fork, sleep и signal (упражнение 11.8). Программа сможет запустить новый процесс с единственной целью — отправить сигнал спустя какое- то время.

Упражнение 11.8 Будильник

В программе alarm.c первая функция, ding, имитирует будильник.

#include <sys/types.h>

#include <signal.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

static int alarm_fired = 0;

void ding(int sig) {

 alarm_fired = 1;

}

В функции main вы заставляете дочерний процесс ждать пять секунд перед отправкой сигнала SIGALRM в свой родительский процесс:

int main() {

 pid_t pid;

 printf("alarm application starting\n");

 pid = fork();

 switch(pid) {

 case -1:

  /* Аварийное завершение */

  perror("fork failed");

  exit(1);

 case 0:

  /* Дочерний процесс */

  sleep(5);

  kill(getppid(), SIGALRM);

  exit(0);

 }

Родительский процесс устроен так, что перехватывает сигнал SIGALRM с помощью вызова signal и затем ждет неизбежности:

 /* Если мы оказались здесь, то мы — родительский процесс */

 printf("waiting for alarm to go off\n");

 (void)signal(SIGALRM, ding);

 pause();

 if (alarm_fired) printf("Ding!\n");

 printf("done\n");

 exit(0);

}

Когда вы выполните программу, то увидите, что она делает паузу на пять секунд, в течение которых ждет имитации будильника:

$ ./alarm

alarm application starting

waiting for alarm to go off

<5 second pause>

Ding!

done $

В этой программе вводится новая функция pause, которая просто приостанавливает выполнение программы до появления сигнала. Когда она получит сигнал, выполняется любой установленный обработчик, и выполнение продолжается как обычно. Она объявляется следующим образом:

#include <unistd.h>

int pause(void);

Функция возвращает -1 (если следующий полученный сигнал не вызвал завершения программы) с переменной errno, равной EINTR, в случае прерывания сигналом. Лучше для ожидания сигналов применять функцию sigsuspend, которую мы обсудим чуть позже в этой главе.

Как это работает

Программа имитации будильника запускает новый процесс вызовом fork. Этот дочерний процесс ожидает пять секунд и затем посылает сигнал SIGALRM своему родителю. Родитель подготавливается к получению сигнала SIGALRM и затем делает паузу до тех пор, пока не будет получен сигнал. Функция printf не вызывается непосредственно в обработчике, вместо этого вы устанавливаете флаг, который проверяете позже.

Применение сигналов и приостановка выполнения — важные составляющие программирования в ОС Linux. Это означает, что программа необязательно должна выполняться все время. Вместо того чтобы долго работать в цикле, проверяя, не произошло ли событие, она может ждать его наступления. Это особенно важно в многопользовательской среде, где процессы совместно используют один процессор, и такой вид деятельного ожидания оказывает большое влияние на производительность системы. Особая проблема, связанная с сигналами, заключается в том, что вы никогда не знаете наверняка, что произойдет, если сигнал появится в середине системного вызова? (Ответ весьма неудовлетворительный: все зависит от ситуации.) Вообще следует беспокоиться только о "медленных" системных вызовах, таких как считывание с терминала, когда системный вызов может вернуться с ошибкой, если сигнал появится во время его пребывания в режиме ожидания. Если вы начнете применять сигналы в своих программах, нужно учитывать, что некоторые системные вызовы могут закончиться аварийно, если сигнал создаст ошибочную ситуацию, которую вы могли не принимать во внимание до того, как добавили обработку сигналов.

Нужно тщательно программировать сигналы, потому что существует ряд "состояний гонок", возникающих в программах, применяющих сигналы. Например, если вы намерены вызвать pause для ожидания сигнала и этот сигнал возникнет до вызова pause, ваша программа может ждать неопределенно долго события, которое не произойдет. Новоиспеченный программист сталкивается с множеством таких состояний гонок, важных проблем синхронизации или согласования времени. Всегда очень внимательно проверяйте программный код, использующий сигналы.

Надежный интерфейс сигналов

Мы рассмотрели подробно возбуждение и перехват сигналов с помощью signal и родственных функций, поскольку они очень часто применяются в старых UNIX-программах. Тем не менее, стандарты X/Open и спецификации UNIX рекомендуют более современный программный интерфейс для сигналов sigaction, который более надежен.

#include <signal.h>

int sigaction<int sig, const struct sigaction *act, struct sigaction *oact);

Структура sigaction, применяемая для определения действий, предпринимаемых при получении сигнала, заданного в аргументе sig, определена в файле signal.h и как минимум включает следующие элементы:

void (*)(int)sa_handler /* функция, SIG_DFL или SIG_IGN */

sigset_t sa_mask        /* сигналы, заблокированные для sa_handler */

int sa_flags            /* модификаторы действий сигнала */

Функция sigaction задает действие, связанное с сигналом sig. Если oact не null, sigaction записывает предыдущее действие для сигнала в указанное oact место. Если act равен null, это все, что делает функция sigaction. Если указатель act не null, задается действие для указанного сигнала.

Как и функция signal, sigaction возвращает 0 в случае успешного выполнения и -1 в случае ошибки. Переменная errno получит значение EINVAL, если заданный сигнал некорректен или была предпринята попытка захватить или проигнорировать сигнал, который нельзя захватывать или игнорировать.

В структуре sigaction, на которую указывает аргумент act, sa_handler — это указатель на функцию, вызываемую при получении сигнала sig. Она очень похожа на функцию func, которая, как вы видели раньше, передавалась функции signal. Вы можете применять специальные значения SIG_IGN и SIG_DFL в поле sa_handler для обозначения того, что сигнал должен игнорироваться или должно быть восстановлено действие по умолчанию, соответственно.

Поле sa_mask описывает множество сигналов, которые будут добавлены в маску сигналов процесса перед вызовом функции sa_handler. Это множество сигналов, которые блокируются и не должны доставляться процессу. Такое поведение мешает возникновению ситуации, описанной ранее, в которой сигнал был получен до того, как его обработчик дошел до завершения. Применение поля sa_mask может устранить это состояние гонок.

Однако сигналы, захватываемые обработчиками, заданными в структуре sigaction, по умолчанию не восстанавливаются, и нужно задать в поле sa_flags значение SA_RESETHAND, если хотите добиться поведения, виденного вами раньше при обсуждении функции signal. Прежде чем обсуждать подробнее sigaction, давайте перепишем программу ctrlc.c, применяя sigaction вместо функции signal (упражнение 11.9).

Упражнение 11.9. Функция sigaction

Внесите приведенные далее изменения, так чтобы сигнал SIGINT перехватывался sigaction. Назовите новую программу ctrlc2.c.

#include <signal.h>

#include <stdio.h>

#include <unistd.h>

void ouch(int sig) {

 printf("OUCH! - I got signal %d\n", sig);

}

int main() {

 struct sigaction act;

 act.sa_handler = ouch;

 sigemptyset(&act.sa_mask);

 act.sa_flags = 0;

 sigaction(SIGINT, &act, 0);

 while (1) {

  printf("Hello World!\n");

  sleep(1);

 }

}

Когда вы выполните эту версию программы, то всегда будете получать сообщение при нажатии комбинации клавиш <Ctrl>+<C>, поскольку SIGINT обрабатывается неоднократно функцией sigaction. Для завершения программы следует нажать комбинацию клавиш <Ctrl>+<\>, которая генерирует по умолчанию сигнал SIIGQUIT.

$ ./ctrlc2

Hello World!

Hello World!

Hello World!

^C

OUCH! - I got signal 2

Hello World!

Hello World!

^C

OUCH! - I got signal 2

Hello World!

Hello World!

^\

Quit

$

Как это работает

Программа вместо функции signal вызывает sigaction для задания функции ouch как обработчика сигнала, возникающего при нажатии комбинации клавиш <Ctrl>+<C> (SIGINT). Прежде всего, она должна определить структуру sigaction, содержащую обработчик, маску сигналов и флаги, В данном случае вам не нужны никакие флаги, и создается пустая маска сигналов с помощью новой функции sigemptyset.

Примечание

После выполнения программы вы можете обнаружить дамп ядра (в файле core). Его можно безбоязненно удалить.

Множества сигналов

В заголовочном файле signal.h определены тип sigset_t и функции, применяемые для манипулирования множествами сигналов. Эти множества используются в sigaction и других функциях для изменения поведения процесса при получении сигналов.

#include <signal.h>

int sigaddset(sigset_t *set, int signo);

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigdelset(sigset_t *set, int signo);

Приведенные функции выполняют операции, соответствующие их названиям, sigemptyset инициализирует пустое множество сигналов. Функция sigfillset инициализирует множество сигналов, заполняя его всеми заданными сигналами, sigaddset и sigdelset добавляют заданный сигнал (signo) в множество сигналов и удаляют его из множества. Они все возвращают 0 в случае успешного завершения и -1 в случае ошибки, заданной в переменной errno. Единственная определенная ошибка EINVAL описывает сигнал как некорректный.

Функция sigismember определяет, включен ли заданный сигнал в множество сигналов. Она возвращает 1, если сигнал является элементом множества, 0, если нет и -1 с errno, равной EINVAL, если сигнал неверный.

#include <signal.h>

int sigismember(sigset_t *set, int signo);

Маска сигналов процесса задается и просматривается с помощью функции sigprocmask. Маска сигналов — это множество сигналов, которые заблокированы в данный момент и не будут приниматься текущим процессом.

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

Функция sigprocmask может изменять маску сигналов процесса разными способами в соответствии с аргументом how. Новые значения маски сигналов передаются в аргументе set, если он не равен null, а предыдущая маска сигналов будет записана в множество сигналов oset.

Аргумент how может принимать одно из следующих значений:

SIG_BLOCK — сигналы аргумента set добавляются к маске сигналов;

SIG_SETMASK —маска сигналов задается аргументом set;

SIG_UNBLOCK — сигналы в аргументе set удаляются из маски сигналов.

Если аргумент set равен null, значение how не используется и единственная цель вызова — перенести значение текущей маски сигналов в аргумент oset.

Если функция sigprocmask завершается успешно, она возвращает 0. Функция вернет -1, если параметр how неверен, в этом случае переменная errno будет равна EINVAL.

Если сигнал заблокирован процессом, он не будет доставлен, но останется ждать обработки. Программа может определить с помощью функции sigpending, какие из заблокированных ею сигналов ждут обработки.

#include <signal.h>

int sigpending(sigset_t *set);

Она записывает множество сигналов, заблокированных от доставки и ждущих обработки, в множество сигналов, на которое указывает аргумент set. Функция возвращает 0 при успешном завершении и -1 в противном случае с переменной errno, содержащей ошибку. Данная функция может пригодиться, когда программе потребуется обрабатывать сигналы и управлять моментом вызова функции обработки.

С помощью функции sigsuspend процесс может приостановить выполнение, пока не будет доставлен один сигнал из множества сигналов. Это более общая форма функции pause, с которой вы уже встречались.

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

Функция sigsuspend замещает маску сигналов процесса множеством сигналов, заданным в аргументе sigmask, и затем приостанавливает выполнение. Оно будет возобновлено после выполнения функции обработки сигнала. Если полученный сигнал завершает программу, sigsuspend никогда не вернет ей управление. Если полученный сигнал не завершает программу, sigsuspend вернет с переменной errno, равной EINTR.

Флаги sigaction

Поле sa_flags структуры sigaction, применяемой в функции sigaction, может содержать значения, изменяющие поведение сигнала (табл. 11.5).

Таблица 11.5

Имя сигнала Описание
SA_NOCLDSTOP Не генерируется SIGCHLD, когда дочерние процессы остановлены
SA_RESETHAND Восстанавливает при получении действие, соответствующее значению SIG_DFL
SA_RESTART Перезапускает прерванные функции вместо ошибки EINTR
SA_NODEFER При перехвате сигнала не добавляет его а маску сигналов

Флаг SA_RESETHAND может применяться для автоматической очистки функции сигнала при захвате сигнала, как мы видели раньше.

Многие системные вызовы, которые использует программа, прерываемые, т.е. при получении сигнала они вернутся с ошибкой и переменная errno получит значение EINTR, чтобы указать, что функция вернула управление в результате получения сигнала. Поведение требует повышенного внимания со стороны приложения, использующего сигналы. Если в поле sa_flags функции sigaction установлен флаг SA_RESTART, функция, которая в противном случае могла быть прервана сигналом, вместо этого будет возобновлена, как только выполнится функция обработки сигнала.

Обычно, когда функция обработки сигнала выполняется, полученный сигнал добавляется в маску сигналов процесса во время работы функции обработки. Это препятствует последующему появлению того же сигнала, заставляющему функцию обработки сигнала выполняться снова. Если функция не реентерабельная, вызов ее другим экземпляром сигнала до того, как она завершит обработку первого сигнала, может создать проблемы. Но если установлен флаг SA_NODEFER, маска сигнала не меняется при получении этого сигнала.

Функция обработки сигнала может быть прервана в середине и вызвана снова чем-нибудь еще. Когда вы возвращаетесь к первому вызову функции, крайне важно, чтобы она все еще действовала корректно. Она должна быть не просто рекурсивной (вызывающей саму себя), а реентерабельной (в нее можно войти и выполнить ее снова). Подпрограммы ядра, обслуживающие прерывания и имеющие дело с несколькими устройствами одновременно, должны быть реентерабельными, поскольку высокоприоритетное прерывание может "войти" в тот код, который выполняется.

Функции, которые безопасно вызываются в обработчике сигнала и в стандарте X/Open гарантированно описанные либо как реентерабельные, либо как самостоятельно не возбуждающие сигналов, перечислены в табл. 11.6.

Все функции, не включенные в табл. 11.6, следует считать небезопасными в том, что касается сигналов.

Таблица 11.6

access alarm cfgetispeed cfgetospeed
cfsetispeed cfsetospeed chdir chmod
chown close creat dup2
dup execle execve exit
fcntl fork fstat getegid
geteuid getgid getgroups getpgrp
getpid getppid getuid kill
link lseek mkdir mkfifo
open pathconf pause pipe
read rename rmdir setgid
setpgid setsid setuid sigaction
sigaddset sigdelset sigemptyset sigfillset
sigismember signal sigpending sigprocmask
sigsuspend sleep stat sysconf
tcdrain tcflow tcflush tcgetattr
tcgetpgrp tcsendbreak tcsetattr tcsetpgrp
time times umask uname
unlink utime wait waitpid
write      

Общая сводка сигналов

В этом разделе мы перечисляем сигналы, в которых нуждаются программы Linux и UNIX для обеспечения стандартных реакций.

Стандартное действие для сигналов, перечисленных в табл. 11.7, — аварийное завершение процесса со всеми последствиями вызова функции _exit (которая похожа на exit, но не выполняет никакой очистки перед возвратом управления ядру). Тем не менее, состояние становится доступным функции wait, а функция waitpid указывает на аварийное завершение, вызванное описанным сигналом.

Таблица 11.7

Имя сигнала Описание
SIGALRM Генерируется таймером, установленным функцией alarm
SIGHUP Посылается управляющему процессу отключающимся терминалом или управляющим процессом во время завершения каждому процессу с высоким приоритетом
SIGINT Обычно возбуждается с терминала при нажатии комбинации клавиш <Ctrl>+<C> или сконфигурированного символа прерывания
SIGKILL Обычно используется из командной оболочки для принудительного завершения процесса с ошибкой, т.к. этот сигнал не может быть перехвачен или проигнорирован
SIGPIPE Генерируется при попытке записи в канал при отсутствии связанного с ним считывателя
SIGTERM Отправляется процессу как требование завершиться. Применяется UNIX при выключении для запроса остановки системных сервисов. Это сигнал, по умолчанию посылаемый командой kill
SIGUSR1, SIGUSR2 Может использоваться процессами для взаимодействия друг с другом, возможно, чтобы заставить их сообщить информацию о состоянии

По умолчанию сигналы, перечисленные в табл. 11.8, также вызывают преждевременное завершение. Кроме того, могут выполняться действия, зависящие от реализации, например, создание файла core.

Таблица 11.8

Имя сигнала Описание
SIGFPE Генерируется исключительной ситуацией во время операций с плавающей точкой
SIGILL Процессор выполнил недопустимую команду. Обычно возбуждается испорченной программой или некорректным модулем совместно используемой памяти
SIGQUIT Обычно возбуждается с терминала при нажатии комбинации клавиш <Ctrl>+<\> или сконфигурированного символа завершения (quit)
SIGSEGV Нарушение сегментации, обычно возбуждается при чтении из некорректного участка памяти или записи в него, а также выход за границы массива или разыменование неверного указателя. Перезапись локального массива и повреждение стека могут вызвать сигнал SIGSEGV при возврате функции по неверному адресу

При получении одного из сигналов, приведенных в табл. 11.9, по умолчанию процесс приостанавливается.

Таблица 11.9

Имя сигнала Описание
SIGSTOP Останавливает выполнение (не может быть захвачен или проигнорирован)
SIGTSTP Сигнал останова терминала часто возбуждается нажатием комбинации клавиш <Ctrl>+<Z>
SIGTTIN, SIGTTOU Применяются командной оболочкой для обозначения того, что фоновые задания остановлены, т.к. им необходимо прочесть данные с терминала или выполнить вывод

Сигнал SIGCONT возобновляет остановленный процесс и игнорируется при получении неостановленным процессом. Сигнал SIGCHLD по умолчанию игнорируется (табл. 11.10).

Таблица 11.10

Имя сигнала Описание
SIGCONT Продолжает выполнение, если процесс остановлен
SIGCHLD Возбуждается, когда останавливается или завершается дочерний процесс

Резюме 

В этой главе вы убедились, что процессы — это основной компонент операционной системы Linux. Вы узнали, как они могут запускаться, завершаться и просматриваться и как вы можете применять их для решения задач программирования. Вы также познакомились с сигналами, которые могут использоваться для управления действиями выполняющихся программ. Вы убедились, что все процессы Linux, вплоть до init включительно, используют одни и те же системные вызовы, доступные любому программисту.

Глава 12

Потоки POSIX

В главе 11 вы видели, как обрабатываются процессы в ОС Linux (и конечно в UNIX). Эти средства обработки множественных процессов долгое время были характерной чертой UNIX-подобных операционных систем. Порой бывает полезно заставить одну программу делать два дела одновременно или, по крайней мере, создать впечатление такой работы. А может быть, вы хотите, чтобы несколько событий произошло одновременно и все они были тесно связаны, но при этом накладные расходы на создание нового процесса с помощью функции fork считаете слишком большими. В таких ситуациях можно применить потоки, позволяющие одному процессу стать многозадачным.

В этой главе мы рассмотрим следующие темы:

□ создание новых потоков в процессе;

□ синхронизацию доступа к данным потоков одного процесса;

□ изменение атрибутов потока;

□ управление в одном и том же процессе одним потоком из другого.

Что такое поток?

Множественные нити исполнения в одной программе называют потоками. Более точно поток — это последовательность или цикл управления в процессе. Все программы, которые вы видели до настоящего момента, выполняли единственный процесс, хотя, как и многие другие операционные системы, ОС Linux вполне способна выполнять множественные процессы одновременно. В действительности у всех процессов есть как минимум один поток исполнения. У всех процессов, с которыми вы пока познакомились в этой книге, был только один поток исполнения.

Важно понять разницу между системным вызовом fork и созданием новых потоков. Когда процесс выполняет системный вызов fork, создается новая копия процесса с ее собственными переменными и собственным PID. Время выполнения этого нового процесса планируется независимо и выполняется он (в основном) независимо от создавшего его процесса. Когда мы создаем в процессе новый поток, этот поток исполнения в противоположность новому процессу получает собственный стек (и, следовательно, локальные переменные), но использует совместно с создавшим его процессом глобальные переменные, файловые дескрипторы, обработчики сигналов и положение текущего каталога.

Идея потоков была популярна какое-то время, но пока Комитет IEEE POSIX не опубликовал некоторые стандарты, потоки не были широко распространены в UNIX-подобных операционных системах и существовавшие реализации разных поставщиков сильно отличались друг от друга. С появлением стандарта POSIX 1003.1c все изменилось; потоки теперь не только лучше стандартизованы, но также реализованы в большинстве дистрибутивов Linux. В наше время многоядерные процессоры стали обычными даже в настольных компьютерах, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им выполнять несколько потоков одновременно. Раньше при наличии одноядерных ЦПУ одновременное исполнение потоков было лишь изобретательной, хотя и очень эффективной иллюзией.

Впервые ОС Linux обзавелась поддержкой потоков около 1996 г. благодаря появлению библиотеки, которую часто называют "LinuxThreads" (потоки Linux). Она почти соответствует стандарту POSIX (на самом деле в большинстве случаев отличия не заметны) и стала важным шагом на пути первого применения потоков программистами Linux. Но между реализацией потоков в Linux и стандартом POSIX есть слабые расхождения, в основном касающиеся обработки сигналов. Ограничения накладываются не столько реализацией библиотеки, сколько низкоуровневой поддержкой ядра Linux.

Разные проекты рассматривали возможности улучшения поддержки потоков в Linux, касающиеся не только устранения слабых расхождений со стандартом POSIX, но и повышения производительности и удаления любых ненужных ограничений. Основная работа была направлена на поиск способов отображения потоков пользовательского уровня на потоки уровня ядра системы. Двумя главными проектами были New Generation POSIX Threads (NGPT, потоки POSIX нового поколения) и Native POSIX Thread Library (NPTL, библиотека истинных потоков POSIX). Оба проекта должны были внести изменения в ядро Linux, обеспечивающие поддержку новых библиотек, и оба предлагали существенное повышение производительности по сравнению с прежней реализацией потоков в Linux.

В 2002 г. команда NGPT объявила, что не хочет разделять сообщество и приостанавливает разработку новых средств для проекта NGPT, но продолжит работу по улучшению поддержки потоков в ОС Linux, присоединив свои усилия к стараниям NPTL. Библиотека NPTL стала новым стандартом для потоков в Linux, выпустив первую основную версию в дистрибутиве Red Hat Linux 9. Вы можете найти интересную основополагающую информацию о NPTL в статье "The Native POSIX Thread Library for Linux" ("Библиотека истинных потоков POSIX для Linux") Ульриха Дреппера (Ulrich Drepper) и Инго Мольнара (Ingo Molnar), которая во время написания книги была доступна в Интернете по адресу http://people.redhat.com/drepper/nptl-design.pdf.

Большая часть программного кода из этой главы будет работать с любой библиотекой потоков, поскольку основана на стандарте POSIX, общем для всех библиотек потоков. Но вы сможете заметить небольшие отличия, если пользуетесь старой версией дистрибутива Linux, особенно когда примените команду ps для просмотра примеров во время их выполнения.

Достоинства и недостатки потоков

В определенных обстоятельствах создание нового потока обладает явно выраженными преимуществами по сравнению с созданием нового процесса. Накладные расходы при создании нового потока существенно меньше, чем при создании нового процесса (несмотря на то, что создание новых процессов в Linux очень эффективно по сравнению с другими операционными системами).

Далее перечислены некоторые достоинства потоков.

□ Иногда очень полезно создать программу, которая выполняет два дела одновременно. Классический пример — подсчет в режиме реального времени слов в документе в ходе редактирования текста. Один поток может управлять пользовательским вводом и выполнять редактирование. Другой, способный видеть то же содержимое документа, может непрерывно обновлять переменную-счетчик количества слов. Первый поток (или даже третий) может использовать эту переменную для информирования пользователя. Другой пример — многопоточный сервер базы данных, в котором единый наблюдаемый процесс обслуживает множество клиентов, улучшая общую пропускную способность за счет обслуживания одних запросов и одновременной блокировки других, ожидающих готовности диска. Серверу базы данных реализовать эту скрытую многозадачность в разных процессах очень трудно, т.к. требования блокировки и непротиворечивости данных приводят к тесной связи двух этих процессов. С помощью множественных потоков воплотить в жизнь этот алгоритм гораздо легче.

□ Производительность приложения, в котором смешаны ввод, вычисления и вывод, можно повысить, запустив эти операции как три отдельных потока. Пока поток ввода или вывода ждет подсоединения, один из оставшихся потоков может продолжить вычисления. Серверное приложение, обрабатывающее многочисленные сетевые подключения, также может подойти для организации программы с множественными потоками.

□ Сейчас, когда многоядерные ЦПУ обычны в настольных и портативных компьютерах, применение множественных потоков внутри процесса может при наличии подходящего приложения позволить одному процессу лучше использовать доступные аппаратные ресурсы.

□ Вообще переключение между потоками требует от операционной системы гораздо меньше усилий, чем переключение между процессами. Таким образом, множественные потоки гораздо менее требовательны к ресурсам, чем множественные процессы, и с ними гораздо практичнее выполнять в однопроцессорных системах программы, логика которых требует применения нескольких потоков исполнения. Считается, что трудности разработки при написании многопоточной программы весьма значительны, и это утверждение нельзя не принимать всерьез.

У потоков есть и недостатки.

□ Создание многопоточной программы требует очень тщательной разработки. Вероятность появления незначительных временных сбоев или ошибок, вызванных нечаянным совместным использованием переменных, в такой программе весьма значительна. Алан Кокс (Alan Сох, всеми уважаемый гуру Linux) сказал, что потоки равнозначны умению "выстрелить в обе собственные ноги одновременно".

□ Отладка многопоточной программы гораздо труднее, чем отладка одного потока исполнения, поскольку взаимосвязи потоков очень трудно контролировать.

□ Программа, в которой громоздкие вычисления разделены на две части, и эти две части выполняются как отдельные потоки, необязательно будет работать быстрее на машине с одним процессором, если только вычисление не позволяет выполнять обе ее части одновременно и у машины, на которой выполняется программа, нет многоядерного процессора для поддержки истинной многопоточности.

Первая программа с применением потоков

Существует целый ряд библиотечных вызовов, связанных с потоками, большинство имен которых начинается с префикса pthread. Для применения этих библиотечных вызовов вы должны определить макрос _REENTRANT, включить файл pthread.h и скомпоновать программу с библиотекой потоков, используя опцию -lpthread.

Когда разрабатывались первые версии библиотечных подпрограмм UNIX и POSIX, предполагалось, что в каждом процессе будет только один поток исполнения. Яркий пример — переменная errno, применяемая для хранения сведений об ошибке после аварийного завершения вызова. В многопоточной программе по умолчанию будет одна переменная errno, совместно используемая всеми потоками. Переменная может легко быть изменена вызовом в одном потоке до того, как другой поток успеет извлечь код предыдущей ошибки. Аналогичные проблемы есть и у функций, таких как fputs, которые, как правило, используют одну глобальную область для буферизации вывода.

Вам нужны реентерабельные подпрограммы. Реентерабельный программный код может вызываться несколько раз либо разными потоками, либо каким-то образом вложенными вызовами и при этом работать корректно. Следовательно, реентерабельная часть программного кода обычно должна применять локальные переменные таким образом, чтобы любой и каждый вызов кода получал собственную уникальную копию данных.

В многопоточных программах вы сообщаете компилятору, что вам нужно это средство, определяя в вашей программе макрос _REENTRANT до любых директив #include. При этом делаются три вещи и столь искусно, что обычно вам даже не нужно знать, какая работа проделана.

□ Некоторые функции получают безопасный реентерабельный вариант прототипа или объявления. При этом имя функции остается обычно прежним, но в конце добавляется суффикс _r, например функция gethostbyname заменяется функцией gethostbyname_r.

□ Некоторые функции из файла stdio.h, которые обычно реализованы как макросы, становятся соответствующими реентерабельными безопасными функциями.

□ Переменная errno из файла errno.h заменяется вызовом функции, которая может определить действительное значение errno безопасным образом с точки зрения многопоточности.

Включение файла pthread.h предоставляет другие прототипы и определения, которые нужны в вашем программном коде, во многом так же, как делает stdio.h для подпрограмм стандартного ввода и вывода. В заключение следует убедиться в том, что вы включили в программу соответствующий заголовочный файл потоков и скомпоновали программу с подходящей библиотекой потоков, в которой реализованы функции семейства pthread. Позже в упражнении данного раздела приведены подробности, касающиеся компиляции вашей программы, но сначала рассмотрим новые функции, необходимые для управления потоками. Функция pthread_create создает новый поток во многом так же, как функция fork создает новый процесс.

#include <pthread.h>

int pthread_create(pthread_t * thread, pthread_attr_t *attr,

 void *(*start_routine)(void *), void *arg);

Прототип выглядит внушительно, но функцию очень легко применять. Первый аргумент — указатель на переменную типа pthread_t. Когда поток создан, в область памяти, на которую указывает эта переменная, записывается идентификатор. Этот идентификатор позволяет ссылаться на поток. Следующий аргумент задает атрибуты потока. Обычно нет нужды в особых атрибутах, и вы можете просто передать в этом аргументе NULL. Позже в этой главе вы увидите, как применять атрибуты потока. В последних двух аргументах потоку передается функция, которую он должен начать выполнять, и аргументы, которые нужно передать этой функции.

void *(*start_routine)(void *)

Предыдущая строка просто говорит о том, что вы должны передать адрес функции, принимающей бестиповой указатель void как параметр, и функция вернет указатель на void. Следовательно, вы можете передать единственный аргумент любого типа и вернуть указатель на любой тип. Применение функции fork заставит продолжить выполнение в том же месте, но с другим кодом возврата, в то время как использование нового потока непосредственно предоставит указатель на функцию, которую новый поток должен начать выполнять.

Возвращаемое значение равно 0 в случае успеха и номеру ошибки, если что-то пошло не так. В интерактивном справочном руководстве есть подробная информация об ошибочных ситуациях для этой и других функций, применяемых в данной главе.

Примечание

pthread_create как большинство функций семейства pthread_ относится к тем немногим функциям Linux, которые не соблюдают соглашение об использовании значения -1 для обозначения ошибок. Если нет полной уверенности, всегда безопаснее всего дважды проверить справочное руководство перед проверкой кода возврата.

Когда поток завершается, он вызывает функцию pthread_exit, во многом так же, как процесс во время завершения вызывает exit. Функция завершает вызванный поток, возвращая указатель на объект. Никогда не применяйте ее для возврата указателя на локальную переменную, потому что переменная перестает существовать, когда поток завершается, вызывая серьезную ошибку. Функция pthread_exit объявляется следующим образом:

#include <рthread.h>

void pthread_exit(void *retval);

Функция pthread_join — эквивалент функции wait, которую процессы применяют для ожидания дочерних процессов. Она объявляется так:

#include <рthread.h>

int pthread_join(pthread_t th, void** thread_return);

Первый параметр — это поток, который следует ждать, идентификатор, который для вас добывает функция pthread_create. Второй аргумент — указатель на указатель, который указывает на возвращаемое из потока значение. Как и pthread_create, эта функция возвращает ноль в случае успешного завершения и код ошибки при сбое.

Выполните упражнение 12.1.

Упражнение 12.1. Простая программа с потоками

Данная программа создает один дополнительный поток, показывает, что он совместно с исходным потоком использует переменные и заставляет новый поток вернуть результат исходному потоку. Далее приведена программа thread1.с.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>

#include <pthread.h>

void *thread_function(void *arg);

char message[] = "Hello World";

int main() {

 int res;

 pthread_t a_thread;

 void *thread_result;

 res = pthread_create(&a_thread, NULL, thread_function, (void *)message);

 if (res ! = 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 printf("Waiting for thread to finish...\n");

 res = pthread_join(a_thread, &thread_result);

 if (res != 0) {

  perror("Thread join-failed");

  exit(EXIT_FAILURE);

 }

 printf("Thread-joined, it returned %s\n", (char *)thread_result);

 printf("Message is now %s\n", message);

 exit(EXIT_SUCCESS);

}

void *thread_function(void *arg) {

 printf("thread_function is running. Argument was %s\n", (char *)arg);

 sleep(3);

 strcpy(message, "Bye!");

 pthread_exit("Thank you for the CPU time");

}

Итак:

1. Перед компиляцией программы вы должны убедиться в том, что определен макрос _REENTRANT. В некоторых системах вы также должны определить _POSIX_C_SOURCE, но обычно в этом нет необходимости.

2. Далее вы должны убедиться в том, что программа скомпонована с подходящей библиотекой потоков. В случае маловероятной ситуации применения старой версии дистрибутива Linux, в которой NPTL не является библиотекой потоков по умолчанию, возможно, у вас возникнет желание обновить ее, хотя большая часть программного кода, приведенного в этой главе, совместима со старой реализацией потоков в Linux. Легкий способ проверить — заглянуть в файл /usr/include/pthread.h. Если в этом файле приведен в качестве даты авторского права (copyright date) 2003 г. или более поздний, почти наверняка у вас реализация NPTL. Если указана более ранняя дата, может быть, самое время получить современную версию дистрибутива Linux.

3. Определив и установив нужные файлы, вы можете откомпилировать и скомпоновать вашу программу следующим образом:

$ cc -D_REENTRANT -I/usr/include/nptl threadl.с -о thread1 -L/usr/lib/nptl -lpthread

Примечание

Если в вашей системе по умолчанию установлена NPTL (что очень вероятно), почти наверняка вам не нужны опции -I и -L, и можно применить более простой вариант:

$ cc -D_REENTRANT thread1.с -о thread1 -lpthread

В данной главе мы будем применять этот более простой вариант строки компиляции.

4. Когда вы выполните эту программу, то увидите следующие строки:

$ ./thread1

Waiting for thread to finish...

thread_function is running. Argument was Hello World

Thread joined, it returned Thank you for the CPU time

Message is now Bye!

Стоит потратить немного времени на анализ данной программы, поскольку мы будем использовать ее как основу в большинстве примеров этой главы.

Как это работает

Вы объявляете прототип функции, которую вызовет поток, когда вы его создадите:

void *thread_function(void *arg);

Как требует функция pthread_create, данная функция принимает в качестве своего единственного параметра указатель на void и возвращает указатель на void. (Мы перейдем к реализации thread_function через минуту.)

В функции main объявлено несколько переменных и затем осуществляется вызов функции pthread_create, чтобы начать выполнение нового потока.

pthread_t a_thread;

void *thread_result;

res = pthread_create(&a_thread, NULL, thread_function, (void *)message);

Вы передаете адрес объекта типа pthread_t, который можете применять в дальнейшем для ссылки на поток. Вы не хотите менять атрибуты потока, заданные по умолчанию, поэтому во втором параметре передаете NULL. Последние два параметра — вызываемая функция и передаваемый ей параметр.

Если вызов завершился нормально, теперь выполняются два потока. Исходный поток (main) продолжается и выполняет код, расположенный следом за функцией pthread_create, а новый поток начинает выполнение в функции, образно названной thread_function.

Исходный поток проверяет, запустился ли новый поток, и затем вызывает функцию pthread_join:

res = pthread_join(a_thread, &thread_result);

Здесь вы передаете идентификатор потока, который ждете, чтобы присоединить, и указатель на результат. Эта функция, прежде чем вернуть управление, будет ждать, пока другой поток не завершится. Затем она выводит возвращаемое из потока значение и содержимое переменной и завершается.

Новый поток начинает выполнение, запуская функцию thread_function, которая выводит свои аргументы, засыпает на короткий период, обновляет глобальные переменные и затем завершается, возвращая строку в поток main. Новый поток пишет в тот же массив message, к которому у исходного потока есть доступ. Если бы вы вызвали функцию fork вместо pthread_create, массив представлял бы собой копию массива message, а не сам массив.

Одновременное выполнение

В упражнении 12.2 показано, как написать программу, которая проверяет одновременное выполнение двух потоков. (Вы, конечно, применяете однопроцессорную систему, ЦП будет искусно переключаться между потоками, а не одновременно выполнять оба потока, используя отдельные ядра процессора аппаратными средствами.) Поскольку вы не встречались еще с какими-либо функциями синхронизации потоков, это будет очень неэффективная программа, делающая нечто, именуемое опросом (polling) двух потоков. И снова вы воспользуетесь тем, что все, за исключением локальных переменных функции, совместно используется двумя потоками в процессе.

Упражнение 12.2. Одновременное выполнение двух потоков

Программа thread2.c в этом упражнении создается за счет небольших изменений программы thread1.c. Вы добавите дополнительную глобальную переменную для определения выполняющегося потока.

Примечание

Файлы с полными текстами примеров можно загрузить с Web-сайта книги.

int run_now = 1;

Задайте run_now равной 1, когда выполняется функция main, и 2, когда выполняется новый поток.

В функцию main после создания нового потока добавьте следующий код:

int print_count1 = 0;

while (print_count1+ < 20) {

 if (run_now == 1) {

  printf("1");

  run_now = 2;

 } else {

  sleep(1);

 }

}

Если переменная run_now равна 1, выведите "1" и присвойте переменной значение 2. В противном случае вы на короткое время засыпаете и снова проверяете значение. Вы ждете, пока значение изменится на 1, проверяя время от времени снова. Этот прием называется циклам активного или деятельного ожидания (busy wait), несмотря, на то, что в данном случае программа засыпает на секунду между очередными проверками. Позже в этой главе вы увидите, как сделать это лучше.

В функции thread_function, где выполняется ваш новый поток, вы делаете примерно то же самое, но с противоположными значениями.

int print_count2 = 0;

while (print_count2++ < 20) {

 if (run_now == 2) {

  printf("2");

  run_now = 1;

 } else {

  sleep(1);

 }

}

Вы удаляете переданные параметр и возвращаемое значение, т.к. они вас больше не интересуют.

Когда вы выполните программу, то увидите следующий вывод. (Вы можете обнаружить, что для формирования вывода, особенно на машине с одноядерным ЦП, программе потребуется несколько секунд.)

$ cc -D_REENTRANT thread2.с -о thread2 -lpthread

$ ./thread2

12121212121212121212

Waiting for thread to finish...

Thread joined

Как это работает

Каждый поток заставляет другой поток выполняться, задавая переменную run_now и затем ожидая, пока другой поток не изменит значение, чтобы можно было продолжить выполнение. Из программы видно, что выполнение переходит от одного потока к другому автоматическими кроме того, она демонстрирует точку, совместно используемую обоими потоками, — переменную run_now.

Синхронизация

В предыдущем разделе вы видели, что два потока выполняются одновременно, но метод переключения между ними топорный и очень неэффективный. К счастью, существует ряд функций, специально разработанных для предоставления лучших способов управления исполнением потоков и доступа к важным фрагментам кода.

В этом разделе мы рассмотрим два основных метода: семафоры, действующие как сторожа, охраняющие фрагменты кода, и мьютексы или исключающие семафоры, действующие как устройство взаимного исключения (отсюда и имя — исключающий семафор) для защиты фрагментов программного кода. На самом деле эти методы похожи, и один может быть описан в терминах другого. Тем не менее существуют ситуации, в которых семантика проблемы делает один более выразительным, чем другой. Например, управление доступом к некоторой области совместно используемой памяти, к которой может обращаться только один поток в каждый момент времени, более естественным кажется исключающий семафор или мьютекс. Для управления доступом к ряду идентичных объектов в целом, например, предоставление потоку одной телефонной линии из набора, включающего пять доступных линий, больше подходит семафор. Какой метод выберите вы, зависит от личных предпочтений и наиболее подходящего для вашей программы алгоритма.

Синхронизация с помощью семафоров

Для семафоров есть два набора интерфейсных функций: один взят из POSIX Realtime Extensions (дополнения POSIX для режима реального времени) и применяется для потоков, а другой, известный как семафоры System V, обычно применяется для синхронизации процессов. (Мы обсудим второй тип в главе 14.) Оба набора не гарантируют взаимозаменяемости и хотя очень похожи, используют вызовы разных функций.

Дейкстра, голландский ученый, специалист по компьютерным наукам, первым сформулировал идею семафоров. Семафор — это переменная особого типа, которая может изменяться с положительным или отрицательным приращением, но обращение к переменной в ответственный момент всегда атомарно даже в многопоточных программах. Это означает, что если два потока (или несколько) в программе пытаются изменить значение семафора, система гарантирует, что все операции будут на самом деле выполняться одна за другой. В случае обычных переменных результат конфликтных операций разных потоков в одной программе произволен.

В этом разделе мы рассмотрим простейший тип семафора, двоичный или бинарный семафор, который принимает только значения 0 и 1. Существует и более обобщенный вид семафора, считающий (counting) семафор, принимающий более широкий диапазон значений. Обычно семафоры используются для защиты фрагмента программного кода, так чтобы только один поток исполнения мог изменить его в любой конкретный момент времени. Для этого нужен двоичный семафор. Порой вам необходимо разрешить ограниченному числу потоков выполнять заданный фрагмент кода, для этого вам следует применять считающий семафор. Поскольку считающие семафоры гораздо менее популярны, мы не будем их обсуждать в дальнейшем, отметив лишь, что они представляют собой логическое расширение двоичного семафора и что реальные вызовы функций должны быть идентичны.

Имена функций семафоров начинаются не с префикса pthread_, как большинство функций, относящихся к потокам, а с sem_. Для работы с потоками применяют четыре базовые функций семафоров. Они все очень просты.

Семафор создается с помощью функции sem_init, которая объявляется следующим образом.

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

Эта функция инициализирует объект-семафор, на который указывает параметр sem, задает вариант его совместного использования (который мы обсудим через минуту) и присваивает ему начальное целочисленное значение. Параметр pshared управляет типом семафора. Если pshared равен 0, семафор локален по отношению к текущему процессу. В противном случае семафор может быть совместно использован разными процессами. Нас сейчас интересуют семафоры, которые не используются совместно разными процессами. Во время написания книги ОС Linux не поддерживала такое совместное использование и передача ненулевого значения параметру pshared приводила к аварийному завершению вызова.

Следующая пара функций управляет значением семафора и объявляется следующим образом.

#include <semaphore.h>

int sem_wait(sem_t* sem);

int sem_post(sem_t* sem);

Обе они принимают указатель на объект-семафор, инициализированный вызовом sem_init.

Функция sem_post атомарно увеличивает значение семафора на 1. Атомарно в данном случае означает, что если два потока одновременно пытаются увеличить значение единственного семафора на 1, они не мешают друг другу, как в случае двух программ, которые читают, увеличивают и записывают значение в файл в одно и то же время. Если обе программы пытаются увеличить значение на 1, семафор всегда будет корректно увеличивать значение на 2.

Функция sem_wait атомарно уменьшает значение семафора на единицу, но всегда ждет до тех пор, пока сначала счетчик семафора не получит ненулевое значение. Таким образом, если вы вызываете sem_wait для семафора со значением 2, поток продолжит выполнение, а семафор будет уменьшен до 1. Если sem_wait вызывается для семафора со значением 0, функция будет ждать до тех пор, пока какой-нибудь другой поток не увеличит значение, и оно станет ненулевым. Если оба потока ждут в функции sem_wait, чтобы один и тот же семафор стал ненулевым, и он увеличивается когда-нибудь третьим потоком, только один из двух ждущих потоков получит возможность уменьшить семафор и продолжиться; другой поток так и останется ждущим. Эта атомарная способность "проверить и установить" в одной функции и делает семафор столь ценным.

Примечание

Есть и другая функция семафора sem_trywait — это неблокирующий партнер sem_wait. Мы не будем ее обсуждать в книге в дальнейшем, дополнительную информацию см. в интерактивном справочном руководстве.

Последняя функция семафоров — sem_destroy. Она очищает семафор, когда вы закончили работу с ним, и объявляется следующим образом:

#include <semaphore.h>

int sem_destroy(gem_t* sem);

И снова эта функция принимает указатель на семафор и очищает любые ресурсы, которые у него могли быть. Если вы попытаетесь уничтожить семафор, которого дожидается какой-либо поток, то получите ошибку.

Как и большинство других, функций, все перечисленные функции возвращают 0 в случае успешного завершения.

А теперь выполните упражнение 12.3.

Упражнение 12.3. Семафор потока

Текст этой программы thread3.c также основан на тексте программы thread1.c. Поскольку изменения значительны, мы приводим новый вариант полностью.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>

#include <pthread.h>

#include <semaphore.h>

void *thread_function(void *arg);

sem_t bin_sem;

#define WORK_SIZE 1024

char work_area[WORK_SIZE];

int main() {

 int res;

 pthread_t a_thread;

 void *thread result;

 res = sem_init(&bin_sem, 0, 0);

 if (res != 0) {

  perror("Semaphore initialization failed");

  exit(EXIT_FAILURE);

 }

 res = pthread_create(&a_thread, NULL, thread_function, NULL);

 if (res != 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 printf("Input some text. Enter 'end' to finish\n");

 while (strncmp("end", work_area, 3) != 0) {

  fgets(work_area, WORK_SIZE, stdin);

  sem_post(&bin_sem);

 }

 printf("\nWaiting for thread to finish...\n");

 res = pthread_join(a_thread, &thread_result);

 if (res != 0) {

  perror("Thread join failed");

  exit(EXIT_FAILURE);

 }

 printf("Thread joined\n");

 sem_destroy(&bin_sem);

 exit(EXIT_SUCCESS);

}

void *thread function(void *arg) { sem_wait(&bin_sem);

 while(strncmp("end", work area, 3) != 0) {

  printf("You input %d characters\n", strlen(work_area)-1);

  sem_wait(&bin_sem);

 }

 pthread_exit(NULL);

}

Первое важное изменение — включение файла semaphore.h для обеспечения доступа к функциям семафоров. Далее вы объявляете семафор и несколько переменных и инициализируете семафор перед тем, как создать новый поток.

sem_t bin_sem;

#define WORK_SIZE 1024

char work_area[WORK_SIZE];

int main() {

 int res;

 pthread_t a_thread;

 void *thread_result;

 res = sem_init(&bin_sem, 0, 0);

 if (res != 0) {

  perror("Semaphore initialization failed");

  exit(EXIT_FAILURE);

 }

Обратите внимание на то, что начальное значение семафора равно 0.

В функции main, после того как вы запустили новый поток, вы читаете некоторый текст с клавиатуры, загружаете вашу рабочую область и затем наращиваете счетчик семафора с помощью sem_post:

 printf("Input some text. Enter 'end' to finish\n");

 while(strncmp("end", work_area, 3) != 0) {

  fgets(work_area, WORK_SIZE, stdin);

  sem_post(&bin_sem);

 }

В новом потоке вы ждете семафор и затем подсчитываете символы ввода:

 sem_wait(&bin_sem);

 while(strncmp("end", work_area, 3) != 0) {

  printf("You input %d characters\n", strlen(work_area)-1);

  sem_wait(&bin_sem);

 }

Пока семафор установлен, вы ждете ввода с клавиатуры. Когда вы получите некоторый ввод, то освобождаете семафор, разрешив второму потоку сосчитать символы перед тем, как первый поток начнет снова считывать ввод с клавиатуры.

И опять потоки совместно используют один и тот же массив work_area. Для того чтобы программный код был короче и за ним легче было следить, мы опять пропустили некоторые проверки ошибок, например значения, возвращаемые из функции sem_wait. Но в рабочем программном коде вы всегда должны проверять ошибочные возвращаемые значения, если нет достаточных оснований для отказа от проверки.

Дайте программе отработать:

$ cc -D_REENTRANT thread3.с -о threads -lpthread

$ ./thread3

Input some text. Enter 'end', to finish

The Wasp Factory

You input 16 characters

Iain Banks

You input 10 characters

end

Waiting for thread to finish...

Thread joined

В программах с потоками временные ошибки всегда трудно найти, но программа кажется приспособленной и к быстрому вводу текста, и более неспешным паузам.

Как это работает

Когда вы инициализируете семафор, то задаете ему начальное значение, равное 0. Следовательно, когда запускается функция потока, вызов sem_wait приостанавливает выполнение и ждет, когда семафор станет ненулевым.

В потоке main вы ждете до тех пор, пока у вас не будет некоторого текста, и затем увеличиваете счетчик семафора с помощью функции sem_post, которая немедленно разрешает другому потоку вернуться из своей функции sem_wait и начать выполнение. После того как он сосчитает символы, поток вновь вызывает sem_wait и приостанавливает выполнение до тех пор, пока поток main не вызовет снова sem_post для того, чтобы увеличить семафор.

Неочевидные недочеты в разработке, которые заканчиваются в результате неявными ошибками, легко пропустить. Давайте слегка изменим программу на thread3a.c, так чтобы вводимый с клавиатуры текст временами заменялся автоматически формируемым текстом. Замените цикл чтения в main следующим:

printf("Input some text. Enter 'end' to finish\n");

while (strncmp("end", work_area, 3) != 0) {

 if (strncmp(work_area, "FAST", 4) == 0) {

  sem_post(&bin_sem);

  strcpy(work_area, "Wheeee...");

 } else {

  fgets(work_area, WORK_SIZE, stdin);

 }

 sem_post(&bin_sem);

}

Теперь, если вы введете FAST, программа вызовет sem_post, чтобы запустить счетчик символов, но немедленно обновит work_area чем-то другим.

$ cc -D_REENTRANT thread3a.с -о thread3a -lpthread

$ ./thread3a

Input some text. Enter 'end' to finish

Excession

You input 9 characters

FAST

You input 7 characters

You input 7 characters

You input 7 characters

end

Waiting for thread to finish...

Thread joined

Проблема этой программы заключается в том, что она рассчитывала на то, что ввод текста из программы продлится так долго, что у другого потока хватит времени для подсчета символов до того, как поток main подготовится передать ему новую порцию текста для подсчета. Когда вы попытались предложить ему два набора слов для подсчета, быстро следующих друг за другом (FAST с клавиатуры и затем Wheeee..., формируемое автоматически), у второго потока не было времени для выполнения. Но семафор наращивался несколько раз, поэтому считающий поток продолжал считать слова и уменьшал значение семафора до тех пор, пока оно снова не стало нулевым.

Этот пример показывает, как аккуратны вы должны быть с временны́ми условиями в многопоточных программах. Исправить программу можно, применяя дополнительный семафор для того, чтобы заставить поток main ждать, пока у считающего потока не появится возможность закончить свой подсчет, но гораздо легче применить мьютекс или исключающий семафор, который мы рассмотрим далее.

Синхронизация с помощью мьютексов

Другой способ синхронизации доступа в многопоточных программах — применение мьютексов (сокращение от mutual exclusions — взаимные исключения) или исключающих семафоров, которые разрешают программистам "запирать" объект так, что только один поток может обратиться к нему.

Базовые функции, необходимые для использования мьютексов, очень похожи на функции семафоров. Они объявляются следующим образом:

#include <рthread.h>

int pthread_mutex_init(pthread_mutex_t* mutex,

 const pthread_mutexattr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex_t* mutex);

int pthread_mutex_unlock(pthread mutex_t* mutex);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

Как обычно, в случае успешного завершения возвращается 0 и код ошибки в случае аварийного завершения, но переменная errno не задается, вам придется использовать код возврата.

Как и функции семафоров, функции мьютексов принимают указатель на предварительно объявленный объект, в данном случае типа pthread_mutex_t. Дополнительный параметр атрибутов в функции pthread_mutex_init позволяет задать атрибуты мьютекса, управляющие его поведением. По умолчанию тип атрибута — "fast". У него есть небольшой недостаток: если ваша программа попытается вызвать функцию pthread_mutex_lock для мьютекса, который уже заблокирован, программа блокируется. Поскольку поток, удерживающий блокировку, в данный момент заблокирован, мьютекс никогда не будет открыт, и программа попадает в тупиковую ситуацию. Есть возможность изменить атрибуты мьютекса так, чтобы он либо проверял наличие такой ситуации и возвращал ошибку, либо действовал рекурсивно и разрешал множественные блокировки тем же самым потоком, если будет такое же количество разблокировок в дальнейшем.

Установка атрибутов мьютекса в этой книге не рассматривается, поэтому мы будем передавать NULL в указателе на атрибуты, и использовать поведение по умолчанию. Дополнительную информацию об изменении атрибутов можно найти в интерактивном справочном руководстве к функции pthread_mutex_init.

Выполните упражнение 12.4. 

Упражнение 12.4. Мьютекс потока

Далее приводится еще одна модификация исходной программы thread1.с, но значительно измененная. На этот раз вы уделите особое внимание доступу к вашим важным переменным и примените мьютекс для того, чтобы быть уверенными в том, что они доступны в любой момент времени только одному потоку. Для легкости чтения текста примера мы пропустили некоторые проверки ошибок при возвратах из мьютекса, заблокированного и открытого. В рабочем программном коде вы обязательно должны проверять эти возвращаемые значения. Далее приведен текст новой программы thread4.c.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>

#include <pthread.h>

#include <semaphore.h>

void *thread_function(void *arg);

pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */

#define WORK_SIZE 1024

char work_area[WORK_SIZE];

int time_to_exit = 0;

int main() {

 int res;

 pthread_t a_thread;

 void *thread_result;

 res = pthread_mutex_init(&work_mutex, NULL);

 if (res != 0) {

  perror("Mutex initialization failed");

  exit(EXIT_FAILURE);

 }

 res pthread_create(&a_thread, NULL, thread_function, NULL);

 if (res != 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 pthread_mutex_lock(&work_mutex);

 printf("Input same text. Enter 'end' to finish\n");

 while (!time_to_exit) {

  fgets (work_area, WORK_SIZE, stdin);

  pthread_mutex_unlock(&work_mutex);

  while(1) {

   pthread_mutex_lock(&work_mutex);

   if (work_area[0] != '\0') {

    pthread_mutex_unlock(&work_mutex);

    sleep(1);

   } else {

    break;

   }

  }

 }

 pthread_mutex_unlock(&work_mutex);

 printf("\nWaiting for thread to finish...\n");

 res = pthread_join(a_thread, &thread_result);

 if (res ! = 0) {

  perror("Thread join failed");

  exit(EXIT_FAILURE);

 }

 printf("Thread joined\n");

 pthread_mutex_destroy(&work_mutex);

 exit(EXIT_SUCCESS);

}

void *thread_function(void *arg) {

 sleep(1);

 pthread_mutex_lock(&work_mutex);

 while(strncmp("end", work_area, 3) ! = 0) {

  printf("You input %d characters\n", strlen(work_area)-1);

  work_area[0] = '\0';

  pthread_mutex_unlock(&work_mutex);

  sleep(1);

  pthread_mutex_lock(&work_mutex);

  while (work_area[0] == '\0') {

   pthread_mutex_unlock(&work_mutex);

   sleep(1);

   pthread_mutex_lock(&work_mutex);

  }

 }

 time_to_exit = 1;

 work_area[0] = '\0';

 pthread_mutex_unlock(&work_mutex);

 pthread_exit(0);

}

После запуска вы получите следующий вывод:

$ cc -D_REENTRANT thread4.с -о thread4 -lpthread

$ ./thread4

Input some text. Enter 'end' to finish

Whit

You input 4 characters

The Crow Road

You input 13 characters

end

Waiting for thread to finish...

Thread joined

Как это работает

Вы начинаете с объявления мьютекса вашей рабочей области и на сей раз дополнительной переменной time_to_exit:

pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */

#define WORK_SIZE 1024

char work_area[WORK_SIZE];

int time_to_exit = 0;

Далее инициализируется мьютекс:

res = pthread_mutex_init(&work_mutex, NULL);

if (res != 0) {

 perror("Mutex initialization failed");

 exit(EXIT_FAILURE);

}

Затем запускается новый поток. Далее приведен код, выполняемый в функции потока:

pthread_mutex_lock(&work_mutex);

while(strncmp("end", work_area, 3) != 0) {

 printf("You input id characters\n", strlen(work_area)-1);

 work_area[0] = '\0';

 pthread_mutex_unlock(&work_mutex);

 sleep(1);

 pthread_mutex_lock(&work_mutex);

 while (work_area[0] == '\0') {

  pthread_mutex_unlock(&work_mutex);

  sleep(1);

  pthread_mutex_lock(&work_mutex);

 }

}

time_to_exit = 1;

work_area[0] = '\0';

pthread_mutex_unlock(&work_mutex);

Сначала новый поток пытается заблокировать мьютекс. Если он уже заблокирован, вызов задерживается до тех пор, пока мьютекс не освободится. После получения доступа вы проверяете, нет ли к вам запроса на завершение выполнения. Если запрашивается завершение, просто задайте переменную time_to_exit, сотрите первый символ в рабочей области и завершите выполнение. 

Если вы не хотите завершать выполнение, сосчитайте символы и очистите первый символ, сделав его пустым (null). Пустой первый символ применяется как способ информирования считывающей программы о завершении подсчета символов. Далее вы открываете мьютекс и ждете выполнения потока main. Периодически вы пытаетесь заблокировать мьютекс и, когда вам это удается, проверяете, подготовил ли поток main новую работу для вас. Если нет, вы открываете мьютекс и ждете какое-то время. Если работа есть, вы считаете символы и выполняете проход цикла снова.

Далее приведен поток main.

pthread_mutex_lock(&work_mutex)

printf("Input some text. Enter 'end' to finish\n");

while (!time_to_exit) {

 fgets(work_area, WORK_SIZE, stdin);

 pthread_mutex_unlock(&work_mutex);

 while(1) {

  pthread_mutex_lock(&work_mutex);

  if (work_area[0] != '\0') {

   pthread_mutex_unlock(&work_mutex);

   sleep(1);

  } else {

   break;

  }

 }

}

pthread_mutex_unlock(&work_mutex);

Он аналогичен второму потоку. Вы блокируете рабочую область и можете читать в нее текст, а затем вы снимаете блокировку, чтобы открыть доступ другому потоку для подсчета слов. Периодически вы блокируете мьютекс, проверяете, сосчитаны ли слова (элемент work_area[0] равен пустому символу), и освобождаете мьютекс, если нужно продолжить ожидание. Как уже отмечалось ранее, этот вид опроса и получения ответа в основном не слишком удачный прием и в реальной жизни вам, возможно, придется применить семафор для его замены. Тем не менее, программный код справляется с задачей демонстрации примера применения мьютекса.

Атрибуты потока

Когда мы начали рассматривать потоки, то не обсуждали более сложную тему — атрибуты потока. Теперь, рассказав о синхронизации потоков — ключевой теме главы, мы можем вернуться назад и остановиться на этих характеристиках потока. Существует лишь несколько атрибутов потока, которыми вы можете управлять; здесь мы собираемся обсудить только те, которые вам понадобятся, скорее всего. Подробную информацию о других атрибутах вы можете найти в интерактивном справочном руководстве.

Во всех предыдущих примерах вы должны были повторно синхронизовать потоки с помощью функции pthread_join, прежде чем разрешить программе завершить выполнение. Это необходимо сделать, если вы хотите, чтобы один поток вернул данные другому потоку, создавшему данный. Иногда вам не нужно ни возвращать информацию из второго потока в поток main, ни заставлять поток main ждать этого.

Предположим, что вы создаете второй поток для записи в буфер резервной копии файла данных, который редактируется, пока поток main продолжает обслуживать пользователя. Когда создание копии закончено, второй поток может тут же завершиться. Ему не нужно присоединяться к потоку main.

Вы можете создать потоки, ведущие себя подобным образом. Они называются отсоединенными или обособленными потоками, и вы создаете их, изменяя атрибуты потока или вызывая функцию pthread_detach. Поскольку мы хотим продемонстрировать атрибуты, то применим здесь первый метод.

Самая важная функция, которая вам понадобится, — pthread_attr_init, инициализирующая объект атрибутов потока:

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);

И снова 0 возвращается в случае успешного завершения и код ошибки в случае аварийного.

Есть и функция для уничтожения: pthread_attr_destroy. Ее задача — обеспечить чистое уничтожение объекта атрибутов. После того как объект уничтожен, он не может быть использован снова до тех пор, пока не будет инициализирован повторно.

Когда вы инициализировали объект атрибутов потока, можно использовать множество дополнительных функций, с помощью которых задается поведение разных атрибутов. Далее перечислены основные из них (полный список вы можете найти в интерактивном справочном руководстве, в разделе, посвященном pthread.h), но мы рассмотрим подробно только два: detechedstate и schedpolicy.

#include <рthread.h>

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

int pthread_attr_getdetachstate(const pthread_attr_t *attr,

 int *detachstate);

int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);

int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int* policy);

int pthread_attr_setschedparam(pthread_attr_t *attr,

 const struct sched_param *param);

int pthread_attr_getschedparam(const pthread_attr_t *attr,

 struct sched_param *param);

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);

int pthread_attr_getinheritsched(const pthread_attr_t *attr,

 int *inherit);

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);

int pthread_attr_setstacksize(pthread_attr_t *attr, int scope);

int pthread_attr_getstacksize(const pthread_attr_t *attr, int* scope);

Как видите, существует лишь несколько атрибутов, которые вы можете применять, но к счастью у вас, как правило, не возникнет необходимости в использовании большинства из них.

□ detachedstate — этот атрибут позволяет избежать необходимости присоединения потоков (rejoin). Как и большинство этих функций с префиксом _set, эта функция принимает указатель на атрибут и флаг для определения требуемого состояния. Два возможных значения флага для функции attr_setdetachstatePTHREAD_CREATE_JOINABLE и PTHREAD_CREATE_DETACHED. По умолчанию у атрибута будет значение PTHREAD_CREATE_JOINABLE, поэтому вы сможете разрешить двум потокам объединяться (один ждет завершения другого). Если задать состояние PTHREAD_CREATE_DETACHED, вы не сможете вызвать функцию pthread_join, чтобы выяснить код завершения другого потока.

□ schedpolicy — этот атрибут управляет планированием потоков. Возможные значения — SCHED_OTHER, SCHED_RR и SCHED_FIFO. По умолчанию атрибут равен SCHED_OTHER. Два других типа планирования доступны только для процессов, выполняющихся с правами суперпользователя, поскольку они оба задают планирование в режиме реального времени, но с немного разным поведением. SCHED_RR использует круговую или циклическую схему планирования, a SCHED_FIFO — алгоритм "первым прибыл, первым обслужен". Оба эти алгоритма не обсуждаются в этой книге.

□ schedparam — это напарник атрибута schedpolicy и позволяет управлять планированием потоков, выполняющихся с типом планирования SCHED_OTHER. Мы рассмотрим пример его применения чуть позже в этой главе.

□ inheritsched — этот атрибут принимает одно из двух значений: PTHREAD_EXPLICIT_SCHED и PTHREAD_INHERIT_SCHED. По умолчанию значение атрибута PTHREAD_EXPLICIT_SCHED, что означает планирование, явно заданное атрибутами. Если задать PTHREAD_INHERIT_SCHED, новый поток будет вместо этого применять параметры, используемые потоком, создавшим его.

□ scope — этот атрибут управляет способом вычисления параметров планирования потока. Поскольку ОС Linux в настоящее время поддерживает единственное значение PTHREAD_SCOPE_SYSTEM, мы не будем рассматривать его в дальнейшем.

□ stacksize — этот атрибут управляет размером стека при создании потока, задается в байтах. Это часть необязательного раздела стандарта и поддерживается только в тех реализациях, у которых определено значение _PTHREAD_THREAD_ATTR_STACKSIZE. Linux по умолчанию реализует потоки со стеком большого размера, поэтому этот атрибут в ОС Linux избыточен.

Выполните упражнение 12.5.

Упражнение 12.5. Установка атрибута отсоединенного состояния

В примере с отсоединенным или обособленным потоком thread5.c вы создаете атрибут потока, задаете состояние потока как отсоединенное и затем создаете с помощью этого атрибута поток. Теперь, когда закончится дочерний поток, он вызовет обычным образом pthread_exit. В это время исходный поток больше не ждет созданный им поток для присоединения. В данном примере используется простой флаг thread_finished, чтобы позволить потоку main определить, закончился ли дочерний поток, и показать, что потоки все еще совместно используют переменные.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <pthread.h>

void *thread_function(void *arg);

char message[] = "Hello World";

int thread_finished = 0;

int main() {

 int res;

 pthread_t a_thread;

 pthread_attr_t thread_attr;

 res = pthread_attr_init(&thread_attr);

 if (res != 0) {

  perror("Attribute creation failed");

  exit(EXIT_FAILURE);

 }

 res = pthread_attr_setdetachstate(&thread_attr,

  PTHREAD_CREATE_DETACHED);

 if (res != 0) {

  perror("Setting detached attribute failed");

  exit(EXIT_FAILURE);

 }

 res = pthread_create(&a_thread, &thread_attr,

  thread_function, (void *)message);

 if (res != 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 (void)pthread_attr_destroy(&thread_attr);

 while (!thread_finished) {

  printf("Waiting for thread to say it's finished...\n");

  sleep(1);

 }

 printf("Other thread finished, bye!\n");

 exit(EXIT_SUCCESS);

}

void *thread_function(void *arg) {

 printf("thread_function is running. Argument was %s\n", (char *)arg);

 sleep(4);

 printf("Second thread setting finished flag, and exiting now\n");

 thread_finished = 1;

 pthread_exit(NULL);

}

Вывод не принесет сюрпризов:

$ ./threads

Waiting for thread to say it's finished...

thread_function is running. Argument was Hello World

Waiting for thread to say it's finished...

Waiting for thread to say it's finished...

Waiting for thread to say it's finished...

Second thread setting finished flag, and exiting now

Other thread finished, bye!

Как видите, установка отсоединенного состояния позволяет второму потоку завершиться независимо, без необходимости исходному потоку ждать этого события.

Как это работает

В исходном тексте программы два важных фрагмента:

pthread_attr_t thread_attr;

res = pthread_attr_init(&thread_attr);

if (res != 0) {

 perror("Attribute creation failed");

 exit(EXIT_FAILURE);

}

который объявляет атрибут потока и инициализирует его, и

res = pthread_attr_setdetachstatе(&thread_attr, PTHREAD_CREATE_DETACHED);

if (res != 0) {

 perror("Setting detached attribute failed");

 exit(EXIT_FAILURE);

}

который устанавливает значения атрибутов для задания отсоединенного состояния потока.

К другим незначительным отличиям относится создание потока с передачей адреса атрибутов:

res = pthread_create(&a_thread, &thread_attr, thread_function, (void*)message);

и для завершенности уничтожение атрибутов после их использования:

pthread_attr_destroy(&thread_attr);

Атрибуты планирования потока

Давайте рассмотрим второй атрибут потока, который вам, возможно, захочется изменить, — атрибут планирования. Изменение этого атрибута очень похоже на установку отсоединенного состояния потока, но есть дополнительные функции, которые можно применять для подбора допустимых уровней приоритета, sched_get_priority_max и sched_get_priority_min.

Выполните упражнение 12.6.

Упражнение 12.6. Планирование

Поскольку данная программа thread6.c очень похожа на программу предыдущего упражнения, мы рассмотрим только отличия.

1. Прежде всего, вам понадобится несколько дополнительных переменных:

int max_priority;

int min_priority;

struct sched_param scheduling_value;

2. После того как установлен атрибут отсоединения, вы задаете политику планирования:

res = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);

if (res != 0) {

 perror("Setting scheduling policy failed");

 exit(EXIT_FAILURE);

}

3. Далее находите диапазон допустимых приоритетов

max_priority = sched_get_priority_max(SCHED_OTHER);

min_priority = sched_get_priority_min(SCHED_OTHER);

и задаете один из них:

scheduling_value.sched_priority = min_priority;

res = pthread_attr_setschedparam(&thread_attr, &scheduling_value);

if (res != 0) {

 perror("Setting scheduling priority failed");

 exit(EXIT_FAILURE);

}

Когда вы запустите программу, то получите следующий вывод:

$ ./thread6

Waiting for thread to say it's finished...

thread_function is running. Argument was Hello World

Waiting for thread to say it's finished...

Waiting for thread to say it's finished...

Waiting for thread to say it's finished...

Second thread setting finished flag, and exiting now

Other thread finished, bye!

Как это работает

Этот пример очень похож на установку атрибута отсоединенного состояния за исключением того, что вы задаете вместо него способ планирования.

Отмена потока

Иногда требуется, чтобы один поток попросил другой завершиться досрочно способом, очень похожим на отправку ему сигнала. Сделать это можно с помощью потоков и параллельно с помощью обработки сигнала; у потоков появляется возможность изменить свое поведение, когда их просят завершиться.

Давайте сначала рассмотрим функцию для создания запроса на завершение потока.

#include <pthread.h>

int pthread_cancel(pthread_t thread);

Она достаточно проста: имея идентификатор потока, вы можете запросить его аннулирование. На приемном конце запроса на отмену все немного сложнее, но не слишком. Поток может установить состояние отмены с помощью функции pthread_setcancelstate.

#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);

Первый параметр равен либо значению PHTREAD_CANCEL_ENABLE, позволяющему получать запросы на отмену, либо PTHREAD_CANCEL_DISABLE, заставляющему игнорировать подобные запросы. Указатель oldstate дает возможность получить предыдущее состояние. Если оно вас не интересует, можно просто передать в этом параметре NULL. Если запросы на отмену принимаются, есть второй уровень управления, принимаемый потоком, — тип отмены, который задается функцией pthread_setcanceltype.

#include <pthread.h>

int pthread_setcanceltype(int type, int *oldtype);

Тип отмены может принимать одно из следующих значений: PTHREAD_CANCEL_ASYNCHRONOUS, заставляющее обрабатывать запросы на отмену немедленно, и PTHREAD_CANCEL_DEFERRED, заставляющее запросы на отмену ждать, пока поток не выполнит одну из следующих функций: pthread_join, pthread_cond_wait, pthread_cond_timedwait, pthread_testcancel, sem_wait или sigwait.

Мы не описываем все эти функции в данной главе, поскольку, как правило, не все они нужны. Когда они понадобятся, вы сможете найти дополнительную информацию на страницах интерактивного справочного руководства.

Примечание

В соответствии со стандартом POSIX системные вызовы, способные задерживать выполнение, такие как read, wait и т.д., должны также быть точками отмены потока. Во время написания книги поддержка этого стандарта в ОС Linux представлялась незавершенной. Но кое-какая работа была проделана, скажем, некоторые задерживающие вызовы, такие как sleep, на самом деле допускают отмену. Для того чтобы обезопасить себя, добавляйте вызовы pthread_testcancel в программный код, который по вашим расчетам может быть отменен.

Параметр oldtype позволяет получить предыдущее состояние, если оно вас не интересует, можно передать NULL. По умолчанию потоки запускаются с состоянием отмены, равным PTHREAD_CANCEL_ENABLE, и типом отмены — PTHREAD_CANCEL_DEFERRED.

Выполните упражнение 12.7.

Упражнение 12.7. Отмена потока

Программа thread7.c — ещё один потомок программы thread1.с. На этот раз основной поток отправляет запрос на отмену потока, который он создал.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <pthread.h>

void *thread_function(void *arg);

int main() {

 int res;

 pthread_t a_thread;

 void *thread_result;

 res = pthread_create(&a_thread, NULL, thread_function, NULL);

 if (res != 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 sleep(3);

 printf("Canceling thread...\n");

 res = pthread_cancel(a_thread);

 if (res != 0) {

  perror("Thread cancelation failed");

  exit(EXIT_FAILURE);

 }

 printf("Waiting for thread to finish...\n");

 res = pthread_join(a_thread, &thread_result);

 if (res != 0) {

  perror("Thread join failed");

  exit(EXIT_FAILURE);

 }

 exit(EXIT_SUCCESS);

}

void *thread_function(void *arg) {

 int i, res;

 res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

 if (res != 0) {

  perror("Thread pthread_setcancelstate failed");

  exit(EXIT_FAILURE);

 }

 res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

 if (res != 0) {

  perror{"Thread pthread_setcanceltype failed");

  exit(EXIT_FAILURE);

 }

 printf("thread_function is running\n");

 for(i = 0; i < 10; i++) {

  printf("Thread is still running (%d)...\n", i);

  sleep(1);

 }

 pthread_exit(0);

}

Когда вы выполните эту программу, то увидите следующий вывод, демонстрирующий отмену потока:

$ ./thread7

thread_function is running

Thread is still running (0)...

Thread is still running (1)...

Thread is still running (2)...

Canceling thread...

Waiting for thread to finish...

$

Как это работает

После того как новый поток был создан обычным способом, основной поток засыпает (чтобы дать новому потоку время для запуска) и затем отправляет запрос на отмену потока.

sleep(3);

printf("Cancelling thread...\n");

res = pthread_cancel(a_thread);

if (res != 0) {

 perror("Thread cancelation failed");

 exit(EXIT_FAILURE);

}

В созданном потоке вы сначала задаете состояние отмены, чтобы разрешить отмену потока:

res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

if (res != 0) {

 perror("Thread pthread_setcancelstate failed");

 exit(EXIT_FAILURE);

}

Далее вы задаете тип отмены PTHREAD_CANCEL_DEFERRED:

res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

if (res != 0) {

 perror("Thread pthread_setcanceltype failed");

 exit(EXIT_FAILURE);

}

И в конце поток ждет отмену:

for (i = 0; i < 10; i++) {

 printf("Thread is still running (%d)...\n", i);

 sleep(1);

}

Потоки в изобилии

До настоящего момента у нас всегда был обычный поток исполнения программы, создающий еще только один поток. Тем не менее мы не хотим, чтобы вы думали, что можно создать только один дополнительный поток (упражнение 12.8).

Упражнение 12.8. Много потоков

В заключительном примере этой главы thread8.c мы покажем, как создать несколько потоков в одной и той же программе и затем собрать их снова в последовательности, отличающейся от порядка их создания.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <pthread.h>

#define NUM_THREADS 6

void *thread_function(void *arg);

int main() {

 int res;

 pthread_t a_thread[NUM_THREADS];

 void *thread_result;

 int lots_of_threads;

 for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

  res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void*)&lots_of_threads);

  if (res != 0) {

   perror("Thread creation failed");

   exit(EXIT_FAILURE);

  }

  sleep(1);

 }

 printf("Waiting for threads' to finish...\n");

 for(lots of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {

  res = pthread_join(a_thread[lots_of_threads], &thread_result);

  if (res == 0) {

   printf("Picked up a thread\n");

  } else {

   perror("pthread_join failed");

  }

 }

 printf("All done\n");

 exit(EXIT_SUCCESS);

}

void *thread_function(void *arg) {

 int my_number = *(int*)arg;

 int rand_num;

 printf("thread_function is running. Argument was %d\n", my_number);

 rand_num = 1 + (int)(9.0*rand() / (RAND_MAX+1.0));

 sleep(rand_num);

 printf("Bye from %d\n", my_number);

 pthread_exit(NULL);

}

Выполнив эту программу, вы получите следующий вывод:

$ ./thread8

thread_function is running. Argument was 0

thread_function is running. Argument was 1

thread_function is running. Argument was 2

thread_function is running. Argument was 3

thread_function is running. Argument was 4

Bye from 1

thread_function is running. Argument was 5

Waiting for threads to finish...

Bye from 5

Picked up a thread

Bye from 0

Bye from 2

Bye from 3

Bye from 4

Picked up a thread

Picked up a thread

Picked up a thread

Picked up a thread

Picked up a thread

All done

Как видите, вы создали много потоков и разрешили им завершаться в произвольной последовательности. В этой программе есть маленькая ошибка, которая проявит себя, если вы удалите вызов sleep из цикла, запускающего потоки. Мы включили ее, чтобы показать, как вы должны быть внимательны при написании программ, применяющих потоки. Вы нашли ее? В следующем разд. "Как это работает" будет дано объяснение.

Как это работает

На сей раз вы создаете массив идентификаторов потоков:

pthread_t a_thread[NUM_THREADS];

и заключаете в цикл создание нескольких потоков:

for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

 res = pthread_create(&(a_thread[lots_of_threads]), NULL,

  thread_function, (void *)&lots_of_threads);

 if (res != 0) {

  perror("Thread creation failed");

  exit(EXIT_FAILURE);

 }

 sleep(1);

}

Затем потоки сами по себе ждут в течение случайного промежутка времени, прежде чем начать выполнение:

void *thread_function(void *arg) {

 int my_number = *(int *)arg;

 int rand_num;

 printf("thread_function is running. Argument was %d\n", my_number);

 rand_num = 1+(int)(9.0* rand()/(RAND_MAX+1.0));

 sleep(randnum);

 printf("Bye from %d\n", my_number);

 pthread_exit(NULL);

}

В это время в основном (исходном) потоке вы ждете, чтобы собрать потоки, но не в том порядке, в каком вы их создали:

for (lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {

 res = pthread_join(a_thread[lots_of__threads], &thread_result);

 if (res == 0) {

  printf("Picked up a thread\n");

 } else {

  perror("pthread_join failed");

 }

}

Если вы попробуете выполнить программу без вызова sleep, то увидите странный эффект: некоторые потоки запускаются с одним и тем же номером, например, вы можете получить вывод, похожий на следующий:

thread_function is running. Argument was 0

thread_function is running. Argument was 2

thread_function is running. Argument was 2

thread_function is running. Argument was 4

thread_function is running. Argument was 4

thread_function is running. Argument was 5

Waiting for threads to finish...

Bye from 5

Picked up a thread

Bye from 2

Bye from 0

Bye from 2

Bye from 4

Bye from 4

Picked up a thread

Picked up a thread

Picked up a thread

Picked up a thread

Picked up a thread

All done

Вы догадались, что произошло? Потоки запускаются, используя локальную переменную как аргумент функции потока. Эта переменная обновляется в цикле. Далее приведены ошибочные строки:

for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

 res = pthread_create(&(a_thread[lots_of_threads]), NULL,

  thread_function, (void *)&lots_of_threads);

Если поток main выполняется достаточно быстро, он может искажать аргумент (lots_of_threads) для некоторых потоков. Поведение, подобное этому, наблюдается, когда недостаточно внимания уделяется совместно используемым переменным и множественным путям исполнения (multiple execution paths). Мы предупреждали вас о том, что программирование потоков требует повышенного внимания при разработке! Для исправления ошибки вам следует передавать непосредственно значение следующим образом:

res = pthread_create(&(a_thread[lots_of_threads]), NULL,

 thread_function, (void *)lots_of_threads);

и конечно изменить thread_function:

void *thread_function(void *arg) {

 int my_number = (int)arg;

Все исправления, выделенные цветом, показаны в программе thread8a.c.

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>

#include <pthread.h>

#define NUM_THREADS 6

void *thread_function(void *arg);

int main() {

 int res;

 pthread_t a_thread[NUM_THREADS];

 void *thread_result;

 int lots_of_threads;

 for (lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {

  res = pthread_create(&(a_thread[lots_of_thread]), NULL,

   thread_function, (void*)lots_оf_threads);

  if (res != 0) {

   perror("Thread creation failed");

   exit(EXIT_FAILURE);

  }

 }

 printf("Waiting for threads to finish...\n");

 for (lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0;

  lots of threads--) {

  res = pthread_join(a_thread[lots_of_threads], &thread_result);

  if (res == 0) {

   printf("Picked up a thread\n");

  } else {

   perror("pthread_join failed");

  }

 }

 printf("All done\n");

 exit(EXIT_SUCCESS);

}

void* thread_function(void* arg) {

 int my_number = (int)arg;

 int rand_num;

 printf("thread_function is running. Argument was %d\n", my_number);

 rand_num = 1+(int)(9.0*rand()/(RAND_MAX+1.0));

 sleep(rand_num);

 printf("Bye from %d\n", my_number);

 pthread_exit(NULL);

}

Резюме

В этой главе вы узнали, как создать несколько потоков исполнения внутри процесса, которые совместно используют глобальные переменные. Вы рассмотрели два способа управления — семафоры и мьютексы, применяемые потоками для доступа к важным фрагментам кода и данным. Далее вы увидели, как управлять атрибутами потоков и, в особенности, как можно отсоединить потоки от основного, не заставляя его ждать завершения созданных им потоков. После краткого обзора способов формирования в одном потоке запросов на отмену других потоков и вариантов управления такими запросами в потоке, получившем их, мы представили программу с множественными одновременно выполняющимися потоками.

Объем книги не позволяет обсудить все до единой функции и тонкости, связанные с потоками, но теперь у вас достаточно знаний для того, чтобы начать писать собственные программы, применяющие потоки, и изучать глубоко скрытые свойства потоков, читая страницы интерактивного справочного руководства.

Глава 13

Связь между процессами: каналы

В главе 11 вы видели очень простой способ пересылки сообщений между процессами с помощью сигналов. Вы формировали уведомляющие события, которые могли бы применяться для вызова ответа, но передаваемая информация была ограничена номером сигнала.

В этой главе вы познакомитесь с каналами, которые позволяют процессам обмениваться более полезной информацией. В конце этой главы вы примените свои вновь приобретенные знания для новой реализации программы, управляющей базой данных компакт-дисков, в виде клиент-серверного приложения.

В данной главе мы обсудим следующие темы:

□ определение канала;

□ каналы процессов;

□ вызовы каналов;

□ родительские и дочерние процессы;

□ именованные каналы — FIFO;

□ замечания, касающиеся клиент-серверных приложений.

Что такое канал?

Мы применяем термин "канал" для обозначения соединения потока данных одного процесса с другим. Обычно вы присоединяете или связываете каналом вывод одного процесса с вводом другого.

Большинство пользователей Linux уже знакомы с идеей конвейера, связывающего вместе команды оболочки так, что вывод одного процесса поставляет данные прямо во ввод другого. В случае команд оболочки это делается с помощью символа конвейера или канала, соединяющего команды следующим образом:

cmd1 | cmd2

Командная оболочка организует стандартный ввод и вывод двух команд так, что:

□ стандартный ввод cmd1 поступает с клавиатуры терминала;

□ стандартный вывод cmd1 поставляется cmd2 как ее стандартный ввод;

□ стандартный вывод cmd2 подсоединен к экрану терминала.

На самом деле командная оболочка заново соединила потоки стандартных ввода и вывода так, что потоки данных проходят с клавиатурного ввода через две команды и выводятся на экран. На рис. 13.1 приведено визуальное представление этого процесса.

Рис. 13.1 

В этой главе вы увидите, как достичь этого эффекта в программе и как можно использовать каналы для связи многих процессов, что позволит создать простую клиент-серверную систему.

Каналы процессов

Возможно, простейший способ передачи данных между программами — применение функций popen и pclose. У них следующие прототипы:

#include <stdio.h>

FILE *popen(const char *command, const char *open_mode);

int pclose(FILE *stream_to_close);

popen

Функция popen позволяет программе запустить другую программу как новый процесс и либо передать ей данные, либо получить их из нее. Строка command — это имя программы для выполнения вместе с любыми параметрами, параметр open_mode должен быть "r" или "w".

Если open_mode"r", вывод вызванной программы становится доступен вызывающей программе и может быть считан из возвращаемого функцией popen файлового потока FILE* с помощью обычных функций библиотеки stdio, предназначенных для чтения (например, fread). Но если open_mode"w", программа может отправить данные вызванной команде с помощью вызова функции fwrite. Далее вызванная программа сможет читать данные из своего стандартного ввода. Обычно вызванная программа не знает, что она считывает данные из другого процесса; она просто читает свой поток стандартного ввода и воздействует на него.

Вызов функции popen должен задавать "r" или "w"; никакого другого значения стандартной реализацией popen не поддерживается. Это означает, что вы не можете вызвать другую программу и одновременно читать из нее и писать в нее. В случае сбоя popen возвращает пустой указатель. Если вы хотите создать двунаправленную связь с помощью каналов, стандартное решение — применить два канала: по одному для потока данных каждого направления.

pclose

Когда процесс, стартовавший с помощью popen, закончится, вы можете закрыть файловый поток, связанный с ним, с помощью функции pclose. Вызов pclose вернет управление, только когда процесс, запущенный с помощью popen, завершится. Если он все еще выполняется во время вызова pclose, вызов pclose будет ждать окончания процесса.

Функция pclose обычно возвращает код завершения процесса, чей файловый поток она закрывает. Если вызывающий процесс уже выполнил оператор wait перед вызовом pclose, статус завершения будет потерян, поскольку вызванный процесс закончен, и функция pclose вернет -1 с переменной errno, получившей значение ECHILD.

Выполните упражнение 13.1.

Упражнение 13.1. Чтение вывода внешней программы

Давайте опробуем простой пример popen1.c с функциями popen и pclose. Вы будете применять в программе popen для доступа к информации из uname. uname — это команда, выводящая системную информацию, включая тип компьютера, имя ОС, версию и выпуск, а также сетевое имя машины.

Запустив программу, вы откроете канал к uname; сделаете его читаемым и зададите read_fp, как указатель на вывод. В конце канал, на который указывает read_fp, закрывается.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 FILE *read_fp;

 char buffer[BUFSIZ +1];

 int chars_read;

 memset(buffer, '\0', sizeof(buffer));

 read_fp = popen("uname -a", "r");

 if (read_fp ! = NULL) {

  chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);

  if (chars_read > 0) {

   printf("Output was:-\n%s\n", buffer);

  }

  pclose(read_fp);

  exit(EXIT_SUCCESS);

 }

 exit(EXIT_FAILURE);

}

Когда вы выполните программу, то должны получить вывод, похожий на следующий (полученный на одной из машин авторов):

$ ./popen1

Output was:-

Linux suse103 2.6.20.2-2-default #1 SMP Fri Mar 9 21:54:10 UTC 2001 i686 i686 i386 GNU/Linux

Как это работает

Программа применяет функцию popen для вызова команды uname с параметром . Затем она использует возвращенный файловый поток для чтения данных, до BUFSIZ символов (как задано в директиве #define из файла stdio.h), и затем выводит их на экран. Поскольку вы перехватываете вывод команды uname внутри программы, его можно обрабатывать.

Отправка вывода в popen

Теперь, когда вы рассмотрели пример захвата вывода из внешней программы, давайте познакомимся с отправкой вывода во внешнюю программу. В упражнении 13.2 показана программа popen2.c, передающая по каналу данные другой программе. В этом примере будет использована команда od (от англ. octal dump — восьмеричный дамп).

Упражнение 13.2. Пересылка вывода в другую программу

Взглянув на следующий программный код, вы увидите, что он очень похож на предыдущий пример, за исключением того, что вы пишете данные в канал вместо чтения данных из него. Далее приведена программа popen2.c.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 FILE *write_fp;

 char buffer[BUFSIZ + 1];

 sprintf(buffer, "Once upon a time, there was...\n");

 write_fp = popen("od -c", "w");

 if (write_fp != NULL) {

  fwrite(buffer, sizeof(char), strlen(buffer), write_fp);

  pclose(write_fp);

  exit(EXIT_SUCCESS);

 }

 exit(EXIT_FAILURE);

}

После выполнения этой программы вы должны получить следующий вывод:

$ ./popen2

0000000  O n c e   u p o n   a   t i m e

0000020  ,   t h e r e   w a s . . . \n

0000037

Как это работает

Программа применяет popen с параметром "w" для запуска команды od -с таким образом, что может отправить данные этой команде. Затем она отправляет строку, которую команда od -с получает и обрабатывает; далее команда od -с выводит результат обработки в своем стандартном выводе.

Такой же вывод можно получить из командной строки с помощью следующей команды:

$ echo "Once upon a time, there was..." | od -c

Передача данных большого объема

Механизм, применявшийся до сих пор, просто отправляет и получает все данные в одном вызове fread или fwrite. Порой вам может понадобиться отправлять данные меньшими порциями или вы не будете знать размера вывода. Для того чтобы не объявлять слишком большой буфер, можно просто применить множественные вызовы fread или fwrite и обрабатывать данные порциями.

В упражнении 13.3 приведена программа popen3.c, читающая все данные из канала.

Упражнение 13.3. Чтение из канала данных большого объема

В этой программе вы читаете данные из вызванного процесса ps ах. У вас нет возможности узнать заранее, какой величины будет вывод, поэтому вы должны разрешить множественные операции чтения из канала.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 FILE * read_fp;

 char buffer[BUFSIZ + 1];

 int chars_read;

 memset(buffer, '\0' , sizeof(buffer));

 read_fp = popen("ps ax", "r");

 if(read_fp != NULL) {

  chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);

  while (chars_read > 0) {

   buffer[chars_read - 1] = '\0';

   printf("Reading %d:-\n %s\n", BUFSIZ, buffer);

   chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);

  }

  pclose(read_fp);

  exit(EXIT_SUCCESS);

 }

 exit(EXIT_FAILURE);

}

Вывод, отредактированный для краткости, подобен приведенному далее:

$ ./popen3

Reading 1024:-

PID TTY  STAT TIME COMMAND

  1 ?    Ss   0:03 init [5]

  2 ?    SW   0:00 [kflushd]

  3 ?    SW   0:00 [kpiod]

  4 ?    SW   0:00 [kswapd]

  5 ?    SW<  0:00 [mdrecoveryd]

...

240 tty2 S    0:02 emacs draft1.txt

Reading 1024:-

368 tty1 S    0:00 ./popen 3

369 tty1 R    0:00 ps -ax

370 ...

Как это работает

Программа применяет функцию popen с параметром "r" аналогично программе popen1.c. В этот раз она продолжает чтение из файлового потока до тех пор, пока в нем есть данные. Учтите, что, хотя программе ps нужно некоторое время для выполнения, Linux так организует планирование процессов, что обе программы выполняются, когда могут. Если у читающего процесса popen3 нет входных данных, он приостанавливается до появления доступных данных. Если записывающий процесс ps формирует вывод, больший по объему, чем может вместить буфер, он приостанавливается до тех пор, пока считывающий процесс не обработает какой-то объем данных.

В этом примере строка Reading:- может не появиться второй раз. Это означает, что BUFSIZ больше объема вывода команды ps. В некоторых (самых современных) системах Linux установлен размер буфера BUFSIZ, равный 8192 байт или даже больше. Для того чтобы проверить корректность работы программы при считывании нескольких порций вывода, попробуйте считывать за один раз меньше символов, чем BUFSIZ, может быть BUFSIZ/10.

Как реализован вызов popen

Вызов popen выполняет программу, которую вы запросили, прежде всего, вызывая командную оболочку sh и передавая ей командную строку как аргумент. У этого процесса две стороны: приятная и не очень.

В ОС Linux (как и во всех UNIX-подобных системах) подстановка всех параметров выполняется командной оболочкой, поэтому вызов оболочки для синтаксического анализа командной строки перед вызовом программы дает возможность командной оболочке выполнить любую подстановку, например, определить реальные файлы, на которые ссылается строка *.с до того, как программа начнет выполняться. Часто это очень полезно и позволяет запускать с помощью popen сложные команды оболочки. Другие функции создания процесса, например execl, гораздо сложнее применять для вызова, поскольку вызывающий процесс должен самостоятельно выполнять подстановки параметров командной оболочки.

Нежелательный эффект применения командной оболочки состоит в том, что для каждого вызова popen вместе с требуемой программой вызывается командная оболочка. Далее каждый вызов popen порождает запуск двух дополнительных процессов, что делает функцию popen немного расточительной с точки зрения расходования системных ресурсов и вызов нужной команды выполняется медленнее, чем было бы в противном случае.

В упражнении 13.4 приведена программа popen4.c, которую можно использовать для демонстрации поведения popen. Вы можете сосчитать количество строк во всех файлах с исходным текстом примеров семейства popen, применив команду cat к файлам и затем пересылая по каналу вывод в команду wc -l, которая считает количество строк. В командной строке эквивалентная команда выглядит следующим образом:

$ cat popen*.c | wc -l

Примечание

На самом деле wc -l popen*.c легче и гораздо эффективнее ввести с клавиатуры, но пример иллюстрирует основные принципы использования каналов.

Упражнение 13.4. Вызов popen запускает командную оболочку

Эта программа применяет в точности предыдущую команду, но с помощью popen, так что она может читать результат.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 FILE *read_fp;

 char buffer[BUFSIZ +1];

 int chars_read;

 memset(buffer, '\0', sizeof(buffer));

 read_fp = popen("cat popen*.с | wc -l", "r");

 if (read_fp != NULL) {

  chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);

  while (chars_read > 0) {

   buffer[chars_read - 1] = '\0';

   printf("Reading:-\n %s\n", buffer);

   chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);

  }

  pclose(read_fp);

  exit(EXIT_SUCCESS);

 }

 exit(EXIT_FAILURE);

}

Выполнив эту программу, вы получите следующий вывод:

$ ./popen4

Reading:-

94

Как это работает

Программа показывает, что вызывается командная оболочка для того, чтобы развернуть popen*.с в список всех файлов, начинающихся с popen и заканчивающихся , а также для обработки символа канала (|) и отправки вывода команды cat в команду . Вы вызываете командную оболочку, программы cat и wc и задаете перенаправление — все в одном вызове popen. Программа, вызвавшая команду, видит только заключительный вывод.

Вызов pipe

Вы познакомились с высокоуровневой функцией popen, а теперь пойдем дальше и рассмотрим низкоуровневую функцию pipe. Она предоставляет средства передачи данных между двумя программами без накладных расходов на вызов командной оболочки для интерпретации запрашиваемой команды. Эта функция также позволит вам лучше управлять чтением и записью данных.

У функции pipe следующее объявление:

#include <unistd.h>

int pipe(int file_descriptor[2]);

Функции pipe передается указатель на массив из двух целочисленных файловых дескрипторов. Она заполняет массив двумя новыми файловыми дескрипторами и возвращает 0. В случае неудачи она вернет -1 и установит переменную errno для указания причины сбоя. В интерактивном справочном руководстве Linux на странице, посвященной функций pipe (в разделе 2 руководства), определены следующие ошибки:

□ EMFILE — процесс использует слишком много файловых дескрипторов;

□ ENFILE — системная таблица файлов полна;

□ EFAULT — некорректный файловый дескриптор.

Два возвращаемых файловых дескриптора подсоединяются специальным образом. Любые данные, записанные в file_descriptor[1], могут быть считаны обратно из file_descriptor[0]. Данные обрабатываются по алгоритму "первым пришел, первым обслужен", обычно обозначаемому как FIFO. Это означает, что если вы записываете байты 1, 2, 3 в file_descriptor[1], чтение из file_descriptor[0] выполняется в следующем порядке: 1, 2, 3. Этот способ отличается от стека, который функционирует по алгоритму "последним пришел, первым обслужен", который обычно называют сокращенно LIFO.

Примечание

Важно уяснить, что речь идет о файловых дескрипторах, а не о файловых потоках, поэтому для доступа к данным вы должны применять низкоуровневые системные вызовы read и write вместо библиотечных функций потоков fread и fwrite.

В упражнении 13.5 приведена программа pipe1.с, которая использует вызов pipe для создания канала.

Упражнение 13.5 Функция pipe

Следующий пример — программа pipe1.c. Обратите внимание на массив file_pipes, который передается функции pipe как параметр.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 int data_processed;

 int filepipes[2];

 const char some_data[] = "123";

 char buffer[BUFSIZ + 1];

 memset(buffer, '\0', sizeof(buffer));

 if (pipe(file_pipes) == 0) {

  data_processed = write(file_pipes[1], some_data, strlen(somedata));

  printf("Wrote %d bytes\n", data_processed);

  data_processed = read(file_pipes[0], buffer, BUFSIZ);

  printf("Read %d bytes: %s\n", data_processed, buffer);

  exit(EXIT_SUCCESS);

 }

 exit(EXIT_FAILURE);

}

Если вы выполните программу, то получите следующий вывод:

$ ./pipe1

Wrote 3 bytes

Read 3 bytes: 123

Как это работает

Программа создает канал с помощью двух файловых дескрипторов из массива file_pipes[]. Далее она записывает данные в канал, используя файловый дескриптор file_pipes[1], и считывает их обратно из file_pipes[0]. Учтите, что у канала есть внутренняя буферизация, позволяющая хранить данные между вызовами функций write и read.

Следует знать, что реакция на попытку писать с помощью дескриптора file_descriptor[0] или читать с помощью дескриптора file_descriptor[1] не определена, поэтому поведение программы может быть очень странным и меняться без каких-либо предупреждений. В системах авторов такие вызовы заканчивались аварийно и возвращали -1, что, по крайней мере, гарантирует легкость обнаружения такой ошибки.

На первый взгляд этот пример использования канала ничего не предлагает такого, чего мы не могли бы сделать с помощью простого файла. Действительные преимущества каналов проявятся, когда вам нужно будет передавать данные между двумя процессами. Как вы видели в главе 11, когда программа создает новый процесс с помощью вызова fork, уже открытые к этому моменту файловые дескрипторы так и остаются открытыми. Создав канал в исходном процессе и затем сформировав с помощью fork новый процесс, вы сможете передать данные из одного процесса в другой через канал (упражнение 13.6).

Упражнение 13.6. Каналы через вызов fork

1. Это пример pipe2.c. Он выполняется также как первый до того момента, пока вы не вызовете функцию fork.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 int data_processed;

 int file_pipes[2];

 const char some_data[] = "123";

 char buffer[BUFSIZ + 1];

 pid_t fork_result;

 memset(buffer, '0', sizeof(buffer));

 if (pipe(file_pipes) == 0) {

  fork_result = fork();

  if (fork_result == -1) {

   fprintf(stderr, "Fork failure");

   exit(EXIT_FAILURE);

  }

2. Вы убедились, что вызов fork отработал, поэтому, если его результат равен нулю, вы находитесь в дочернем процессе:

  if (fork_result == 0) {

   data_processed = read(file_pipes[0], buffer, BUFSIZ);

   printf("Read %d bytes: %s\n", data_processed, buffer);

   exit(EXIT_SUCCESS);

  }

3. В противном случае вы должны быть в родительском процессе:

  else {

   data_processed = write(file_pipes[1], some_data,

    strlen(some_data));

   printf("Wrote %d bytes\n", data_processed);

  }

 }

 exit(EXIT_SUCCESS);

}

После выполнения этой программы вы получите вывод, аналогичный предыдущему:

$ ./pipe2

Wrote 3 bytes

Read 3 bytes: 123

Вы можете столкнуться с повторным выводом строки приглашения для ввода команды перед завершающим фрагментом вывода, поскольку родительский процесс завершится раньше дочернего, поэтому мы подчистили вывод, чтобы его легче было читать.

Как это работает

Сначала программа создает канал с помощью вызова pipe. Далее она применяет вызов fork для создания нового процесса. Если fork завершился успешно, родительский процесс пишет данные в канал, в то время как дочерний считывает данные из канала. Оба процесса, и родительский, и дочерний, завершаются после одного вызова write и read. Если родительский процесс завершается раньше дочернего, вы можете увидеть между двумя выводами строку приглашения командной оболочки.

Несмотря на то, что программа внешне похожа на первый пример pipe, мы сделали большой шаг вперед, получив возможность использовать разные процессы для чтения и записи (рис. 13.2).

Рис. 13.2 

Родительский и дочерний процессы

Следующий логический шаг в нашем изучении вызова pipe — разрешить дочернему процессу быть другой программой, отличной от своего родителя, а не просто другим процессом, выполняющим ту же самую программу. Сделать это можно с помощью вызова exec. Единственная сложность заключается в том, что новому процессу, созданному exec, нужно знать, какой файловый дескриптор применять для доступа. В предыдущем примере этой проблемы не возникло, потому что дочерний процесс обращался к своей копии данных file_pipes. После вызова exec возникает другая ситуация, поскольку старый процесс заменен новым дочерним процессом. Эту проблему можно обойти, если передать файловый дескриптор (который, в конце концов, просто число) как параметр программе, вновь созданной с помощью вызова exec.

Для того чтобы посмотреть, как это работает, вам понадобятся две программы (упражнение 13.7). Первая — поставщик данных. Она создает канал и затем вызывает дочерний процесс, потребитель данных.

Упражнение 13.7. Каналы и exec

1. Для получения первой программы исправьте pipe2.c, превратив ее в pipe3.c. Измененные строки затенены.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 int data_processed;

 int file_pipes[2];

 const char somedata[] = "123";

 char buffer[BUFSIZ + 1];

 pid_t fork_result;

 memset(buffer, '\0', sizeof(buffer));

 if (pipe(file_pipes) == 0) {

  fork_result = fork();

  if (fork_result == (pid_t)-1) {

   fprintf(stderr, "Fork failure");

   exit(EXIT_FAILURE);

  }

  if (fork_result == 0) {

   sprintf(buffer, "%d", file_pipes[0]);

   (void)execl("pipe4", "pipe4", buffer, (char*)0);

   exit(EXIT_FAILURE);

  } else {

   data_processed = write(file_pipes[1], some_data, strlen(some_data));

   printf ("%d - wrote %d bytes\n", getpid(), data_processed);

  }

 }

 exit(EXIT_SUCCESS);

}

2. Программа-потребитель pipe4.c, читающая данные, гораздо проще:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main(int argc, char *argv[]) {

 int data_processed;

 char buffer[BUFSIZ + 1];

 int file_descriptor;

 memset(buffer, '\0', sizeof(buffer));

 sscanf(argv[1], "%d", &file_descriptor);

 data_processed = read(file_descriptor, buffer, BUFSIZ);

 printf("%d — read %d bytes: %s\n", getpid(), data_processed,

  buffer);

 exit(EXIT_SUCCESS);

}

Выполнив pipe3 и помня о том, что она вызывает программу pipe4, вы получите вывод, аналогичный приведенному далее:

$ ./pipe3

22460 - wrote 3 bytes

22461 - read 3 bytes: 123

Как это работает

Программа pipe3 начинается как предыдущий пример, используя вызов pipe для создания канала и затем вызов fork для создания нового процесса. Далее она применяет функцию sprintf для сохранения в буфере номера файлового дескриптора чтения из канала, который формирует аргумент программы pipe4.

Вызов execl применен для вызова программы pipe4. В нем использованы следующие аргументы:

□ вызванная программа;

□ argv[0], принимающий имя программы;

□ argv[1], содержащий номер файлового дескриптора, из которого программа должна читать;

□ (char *)0, завершающий список параметров.

Программа pipe4 извлекает номер файлового дескриптора из строки аргументов и затем читает из него данные.

Чтение закрытых каналов

Прежде чем двигаться дальше, необходимо более внимательно рассмотреть файловые дескрипторы, которые открыты. До этого момента вы разрешали читающему процессу просто читать какие-то данные и завершаться, полагая, что ОС Linux уберет файлы в ходе завершения процесса.

В большинстве программ, читающих данные из стандартного ввода, это делается несколько иначе, чем в виденных вами до сих пор примерах. Обычно программы не знают, сколько данных они должны считать, поэтому они, как правило, выполняют цикл — чтение данных, их обработка и затем снова чтение данных и так до тех пор, пока не останется данных для чтения.

Вызов read обычно будет задерживать выполнение процесса, т.е. он заставит процесс ждать до тех пор, пока не появятся данные. Если другой конец канала был закрыт, следовательно, нет ни одного процесса, имеющего канал для записи, и вызов read блокируется. Поскольку это не очень полезно, вызов read, пытающийся читать из канала, не открытого для записи, возвращает 0 вместо блокирования. Это позволит читающему процессу обнаружить канальный эквивалент метки "конец файла" и действовать соответствующим образом. Учтите, что это не то же самое, что чтение некорректного дескриптора файла, которое вызов read считает ошибкой и обозначает возвратом -1.

Если вы применяете канал с вызовом fork, есть два файловых дескриптора, которые можно использовать для записи в канал: один в родительском, а другой в дочернем процессах. Вы должны закрыть файловые дескрипторы записи в канал в обоих этих процессах, прежде чем канал будет считаться закрытым и вызов read для чтения из канала завершится аварийно. Мы рассмотрим пример этого позже, когда вернемся к данной теме, для того чтобы подробно обсудить флаг O_NONBLOCK и каналы FIFO.

Каналы, применяемые как стандартные ввод и вывод

Теперь, когда вы знаете, как заставить вызов read, примененный к пустому каналу, завершиться аварийно, можно рассмотреть более простой метод соединения каналом двух процессов. Вы устраиваете так, что у одного из файловых дескрипторов канала будет известное значение, обычно стандартный ввод, 0, или стандартный вывод, 1. Его немного сложнее установить в родительском процессе, но при этом значительно упрощается программа дочернего процесса.

Одно неоспоримое достоинство заключается в том, что вы можете вызывать стандартные программы, которым не нужен файловый дескриптор как параметр. Для этого вам следует применить функцию dup, с которой вы встречались в главе 3. Существуют две тесно связанные версии функции dup, которые объявляются следующим образом:

#include <unistd.h>

int dup(int file_descriptor);

int dup2(int file_descriptor_one, int file_descriptor_two);

Назначение вызова dup — открыть новый дескриптор файла, немного похоже на то, как это делает вызов open. Разница в том, что файловый дескриптор, созданный dup, ссылается на тот же файл (или канал), что и существующий файловый дескриптор. В случае вызова dup новый файловый дескриптор всегда имеет самый маленький доступный номер, а в случае dup2 — первый доступный дескриптор, больший чем значение параметра file_descriptor_two.

Примечание

Того же эффекта, что и применение вызовов dup и dup2 можно добиться, применяя более общий вызов fcntl с командой F_DUPFD. Как говорилось, вызов dup легче использовать, поскольку он разработан специально для создания дубликатов файловых дескрипторов. Он также очень широко применяется, поэтому вы встретите его гораздо чаще в существующих программах, чем вызов fcntl и команду F_DUPFD.

Итак, как же dup помогает в обмене данными между процессами? Хитрость кроется в знании того, что дескриптор стандартного файла ввода всегда 0 и что dup всегда возвращает новый файловый дескриптор, применяя наименьший доступный номер. Сначала закрыв дескриптор 0, а затем вызвав dup, вы получите новый файловый дескриптор с номером 0. Поскольку новый файловый дескриптор — это дубликат существующего, стандартный ввод изменится и получит доступ к файлу или каналу, файловый дескриптор которого вы передали в функцию dup. В результате вы создадите два файловых дескриптора, которые ссылаются на один и тот же файл или канал и один из них будет стандартным вводом.

Управление файловым дескриптором с помощью close и dup

Легче всего понять, что происходит, когда вы закрываете файловый дескриптор 0 и затем вызываете dup, если рассмотреть состояние первых четырех файловых дескрипторов, изменяющихся последовательно друг за другом (табл. 13.1).

Таблица 13.1

Номер файлового дескриптора Первоначально После закрытия файлового дескриптора 0 После вызова dup
0 Стандартный ввод {closed} Файловый дескриптор канала
1 Стандартный вывод Стандартный вывод Стандартный вывод
2 Стандартный поток ошибок Стандартный поток ошибок Стандартный поток ошибок
3 Файловый дескриптор канала Файловый дескриптор канала Файловый дескриптор канала

А теперь выполните упражнение 13.8.

Упражнение 13.3. Каналы и dup

Давайте вернемся к предыдущему примеру, но на этот раз вы измените дочернюю программу, заменив в ней файловый дескриптор stdin концом считывания read созданного вами канала. Вы также выполните некоторую реорганизацию файловых дескрипторов, чтобы дочерняя программа могла правильно определить конец данных в канале. Как обычно, мы пропустили некоторые проверки ошибок для краткости.

Превратите программу pipe3.c в pipe5.c с помощью следующего программного кода:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main() {

 int data_processed;

 int file pipes[2];

 const char some_data[] = "123";

 pid_t fork_result;

 if (pipe(file_pipes) == 0) {

  fork_result = fork();

  if (fork_result == (pid_t)-1) {

   fprintf(stderr, "Fork failure");

   exit(EXIT_FAILURE);

  }

  if (fork_result == (pid_t)0) {

   close(0);

   dup(file_pipes[0];

   close(file_pipes[0]);

   close(file_pipes[1]);

   execlp("od", "od", "-c", (char*)0);

   exit(EXIT_FAILURE);

  } else {

   close(file_pipes[0]);

   data_processed = write(file_pipes[1], some_data,

    strlen(some_data));

   close(file_pipes[1]);

   printf("%d — wrote %d bytes\n", (int)getpid(), data_processed);

  }

 }

 exit(EXIT_SUCCESS);

}

У этой программы следующий вывод:

$ ./pipe5

22495 - wrote 3 bytes

0000000 1 2 3

0000003

Как это работает

Как и прежде, программа создает канал, затем выполняет вызов fork, создавая дочерний процесс. В этот моменту обоих процессов, родительского и дочернего, есть файловые дескрипторы для доступа к каналу, по одному для чтения и записи, т.е. всего четыре открытых файловых дескриптора.

Давайте первым рассмотрим дочерний процесс. Он закрывает свой стандартный ввод с помощью close(0) и затем вызывает dup(file_pipes[0]). Этот вызов дублирует файловый дескриптор, связанный с концом read канала, как файловый дескриптор 0, стандартный ввод. Далее дочерний процесс закрывает исходный файловый дескриптор для чтения из канала, file_pipes[0]. Поскольку этот процесс никогда не будет писать в канал, он также закрывает файловый дескриптор для записи в канал, file_pipes[1]. Теперь у дочернего процесса единственный файловый дескриптор, связанный с каналом, файловый дескриптор 0, его стандартный ввод.

Далее дочерний процесс может применить exec для вызова любой программы, которая читает стандартный ввод. В данном случае мы используем команду od. Команда od будет ждать, когда данные станут ей доступны, как если бы она ждала ввода с терминала пользователя. В действительности без специального программного кода, позволяющего непосредственно выяснить разницу, она не будет знать, что ввод приходит из канала, а не с терминала.

Родительский процесс начинает с закрытия конца чтения канала, file_pipes[0], потому что он никогда не будет читать из канала. Затем он пишет данные в канал. Когда все данные записаны, родительский процесс закрывает конец записи в канал и завершается. Поскольку теперь нет файловых дескрипторов, открытых для записи в канал, программа od сможет считать три байта, записанных в канал, но последующие операции чтения далее будут возвращать 0 байтов, указывая на конец файла. Когда read вернет 0, программа od завершится. Это аналогично выполнению команды od, введенной с терминала, и последующему нажатию комбинации клавиш <Ctrl>+<D> для отправки признака конца файла команде od.

На рис. 13.3 показан результат вызова pipe, на рис. 13.4 — результат вызова fork, а на рис. 13.5 представлена программа, когда она готова к передаче данных.

Рис. 13.3

Рис. 13.4

Рис. 13.5

Именованные каналы: FIFO

До сих пор вы могли передавать данные только между связанными программами, т.е. программами, которые стартовали из общего процесса-предка. Часто это очень неудобно, хотелось бы, чтобы и у несвязанных процессов была возможность обмениваться данными.

Вы можете сделать это с помощью каналов FIFO, часто называемых именованными каналами. Именованный канал — это файл специального типа (помните, что в ОС Linux все, что угодно, — файл!), существующий в виде имени в файловой системе, но ведущий себя как неименованные каналы, которые вы уже встречали.

Вы можете создавать именованные каналы из командной строки и внутри программы. С давних времен программой создания их в командной строке была команда mknod:

$ mknod имя_файла p

Однако команды mknod нет в списке команд X/Open, поэтому она включена не во все UNIX-подобные системы. Предпочтительнее применять в командной строке

$ mkfifo имя_файла

Примечание

У некоторых более старых версий UNIX была только команда mknod. В стандарте X/Open issue 4 Version 2 есть вызов функции mknod, но не программа командной строки. ОС Linux, как всегда настроенная дружелюбно, предлагает оба варианта: mknod и mkfifo.

Внутри программы можете применять два разных вызова:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);

int mknod(const char* filename, mode_t mode | S_IFIFO, (dev_t)0);

Помимо команды mknod вы можете использовать функцию mknod для создания файлов специальных типов. Единственный переносимый вариант применения этой функции, создающий именованный канал, — использование значения 0 типа dev_t и объединений с помощью операции or режима доступа к файлу и S_IFIFO. В примерах мы будем применять более простую функцию mkfifo.

Итак, выполните упражнение 13.9.

Упражнение 13.9. Создание именованного канала

Далее приведен исходный текст примера fifo1.c.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

int main() {

 int res = mkfifo("/tmp/my_fifo", 0777);

 if (res == 0) printf ("FIFO created\n");

 exit(EXIT_SUCCESS);

}

Вы можете создать канал и заглянуть в него:

$ ./fifo1

FIFO created

$ ls -lF /tmp/my_fifo

prwxr-xr-x 1 rick users 0 2007-06-16 17:18 /tmp/my_fifo|

Обратите внимание на то, что первый символ вывода — р, обозначающий канал. Символ | в конце добавлен опцией -F команды ls и тоже обозначает канал.

Как это работает

Программа применяет функцию mkfifo для создания специального файла. Несмотря на то, что запрашиваете режим 0777, он заменяется пользовательской маской (umask), устанавливаемой (в данном случае 022) точно так же, как при создании обычного файла, поэтому у результирующего файла режим 755. Если ваша umask установлена иначе, например, ее значение 0002, вы увидите другие права доступа у созданного файла.

Удалить FIFO можно как традиционный файл с помощью команды rm или внутри программы посредством системного вызова unlink.

Доступ к FIFO

У именованных каналов есть одно очень полезное свойство: поскольку они появляются в файловой системе, их можно применять в командах на месте обычного имени файла. Прежде чем вы продолжите программирование с использованием созданного вами файла FIFO, давайте исследуем поведение такого файла с помощью обычных команд для работы с файлом (упражнение 13.10).

Упражнение 13.10. Организации доступа к файлу FIFO

1. Сначала попробуйте прочесть (пустой) файл FIFO:

$ cat < /tmp/my_fifo

2. Теперь попытайтесь записать в FIFO. Вам придется использовать другой терминал, поскольку первая команда в данный момент "зависла" в ожидании появления каких-нибудь данных в FIFO:

$ echo "Hello World" > /tmp/my_fifo

Вы увидите вывод команды cat. Если не посылать никаких данных в канал FIFO, команда cat будет ждать до тех пор, пока вы не прервете ее выполнение, традиционно комбинацией клавиш <Ctrl>+<C>.

3. Можно выполнить обе команды одновременно, переведя первую в фоновый режим:

$ cat < /tmp/my_fifo &

[1] 1316

$ echo "Hello World" > /tmp/my_fifo

Hello World

[1]+ Done   cat </tmp/my_fifo

$

Как это работает

Поскольку в канале FIFO не было данных, обе команды, cat и echo, приостанавливают выполнение, ожидая, соответственно, поступления каких-нибудь данных и какого-либо процесса для их чтения.

На третьем шаге процесс cat с самого начала заблокирован в фоновом режиме. Когда echo делает доступными некоторые данные, команда cat читает их и выводит в стандартный вывод. Обратите внимание на то, что она затем завершается, не дожидаясь дополнительных данных. Программа cat не блокируется, т.к. канал уже закрылся, когда завершилась вторая команда, поместившая данные в FIFO, поэтому вызовы read в программе cat вернут 0 байтов, обозначая этим конец файла.

Теперь, когда вы посмотрели, как ведут себя каналы FIFO при обращении к ним с помощью программ командной строки, давайте рассмотрим более подробно программный интерфейс, предоставляющий больше возможностей управления операциями чтения и записи при организации доступа к FIFO.

Примечание

В отличие от канала, созданного вызовом pipe, FIFO существует как именованный файл, но не как открытый файловый дескриптор, и должен быть открыт перед тем, как можно будет из него читать данные или в него записывать их. Открывается и закрывается канал FIFO с помощью функций open и close, которые вы ранее применяли к файлам, но с дополнительными функциональными возможностями. Вызову open передается полное имя FIFO вместо полного имени обычного файла.

Открытие FIFO с помощью open

Основное ограничение при открытии канала FIFO состоит в том, что программа не может открыть FIFO для чтения и записи с режимом O_RDWR. Если программа нарушит это ограничение, результат будет непредсказуемым. Это очень разумное ограничение, т.к., обычно канал FIFO применяется для передачи данных в одном направлении, поэтому нет нужды в режиме O_RDWR. Процесс стал бы считывать обратно свой вывод, если бы канал был открыт для чтения/записи.

Если вы действительно хотите передавать данные между программами в обоих направлениях, гораздо лучше использовать пару FIFO или неименованных каналов, по одному для каждого направления передачи, или (что нетипично) явно изменить направление потока данных, закрыв и снова открыв канал FIFO. Мы вернемся к двунаправленному обмену данными с помощью каналов FIFO чуть позже в этой главе.

Другое различие между открытием канала FIFO и обычного файла заключается в использовании флага open_flag (второй параметр функции open) со значением O_NONBLOCK. Применение этого режима open изменяет способ обработки не только вызова open, но и запросов read и write для возвращаемого файлового дескриптора.

Существует четыре допустимых комбинации значений O_RDONLY, O_WRONLY и O_NONBLOCK флага. Рассмотрим их все по очереди.

open(const char *path, O_RDONLY);

В этом случае вызов open блокируется, он не вернет управление программе до тех пор, пока процесс не откроет этот FIFO для записи. Это похоже на первый пример с командой cat.

open(const char *path, O_RDONLY | O_NONBLOCK);

Теперь вызов open завершится успешно и вернет управление сразу, даже если канал FIFO не был открыт для записи каким-либо процессом.

open(const char *path, O_WRONLY);

В данном случае вызов open будет заблокирован до тех пор, пока процесс не откроет тот же канал FIFO для чтения.

open(const char *path, O_WRONLY | O_NONBLOCK);

Этот вариант вызова всегда будет возвращать управление немедленно, но если ни один процесс не открыл этот канал FIFO для чтения, open вернет ошибку, -1, и FIFO не будет открыт. Если есть процесс, открывший FIFO для чтения, возвращенный файловый дескриптор может использоваться для записи в канал FIFO.

Примечание

Обратите внимание на асимметрию в использовании O_NONBLOCK с O_RDONLY и O_WRONLY, заключающуюся в том, что неблокирующий вызов open для записи завершается аварийно, если ни один процесс не открыл канал для чтения, а неблокирующий вызов open для чтения не возвращает ошибку. На поведение вызова close флаг O_NONBLOCK влияния не оказывает.

Выполните упражнение 13.11.

Упражнение 13.11. Открытие файлов FIFO

Теперь рассмотрим, как можно использовать поведение вызова open с флагом, содержащим O_NONBLOCK, для синхронизации двух процессов. Вместо применения нескольких программ-примеров вы напишите одну тестовую программу fifo2.c, которая позволит исследовать поведение каналов FIFO при передаче ей разных параметров.

1. Начните с заголовочных файлов, директивы #define и проверки правильности количества предоставленных аргументов командной строки:

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <fcntl.h>

#include <sys/types.h>

#include <sys/stat.h>

#define FIFO_NAME "/tmp/my_fifo"

int main(int argc, char *argv[]) {

 int res;

 int open_mode = 0;

 int i;

 if (argc < 2) {

  fprintf(stderr, "Usage: %s <some combination of\

   O_RDONLY O_WRONLY O_NONBLOCK>\n", *argv);

  exit(EXIT_FAILURE);

 }

2. Полагая, что программа передает тестовые данные, вы задаете параметр open_mode из следующих аргументов:

 for(i = 1; i <argc; i++) {

  if (strncmp(*++argv, "O_RDONLY", 8) == 0) open_mode |= O_RDONLY;

  if (strncmp(*argv, "O_WRONLY", 8) == 0) open_mode |= O_WRONLY;

  if (strncmp(*argv, "O_NONBLOCK", 10) == 0) open_mode |= O_NONBLOCK;

 }

3. Далее проверьте, существует ли канал FIFO, и при необходимости создайте его. Затем FIFO открывается, и пока программа засыпает на короткое время, выполняется результирующий вывод. В заключение FIFO закрывается.

 if (access(FIFO_NAME, F_OK) == -1) {

  res = mkfifo(FIFO_NAME, 0777);

  if (res != 0) {

   fprintf(stderr, "Gould not create fifo %s\n", FIFO_NAME);

   exit(EXIT_FAILURE);

  }

 }

 printf("Process %d opening FIF0\n", getpid());

 res = open(FIFO_NAME, open_mode);

 printf("Process %d result %d\n", getpid(), res);

 sleep(5);

 if (res != -1) (void)close(res);

 printf("Process %d finished\n", getpid());

 exit(EXIT_SUCCESS);

}

Как это работает

Эта программа позволяет задать в командной строке комбинации значений O_RDONLY, O_WRONLY и O_NONBLOCK, которые вы хотите применить. Делается это сравнением известных строк с параметрами командной строки и установкой (с помощью |=) соответствующего флага при совпадении строки. В программе используется функция access, проверяющая, существует ли уже файл FIFO, и создающая его при необходимости.

Никогда не уничтожайте FIFO, т.к. у вас нет способа узнать, не использует ли FIFO другая программа.

O_RDONLY и O_WRONLY без O_NONBLOCK

Теперь у вас есть тестовая программа, и вы можете проверить комбинации пар. Обратите внимание на то, что первая программа, считыватель, помещена в фоновый режим.

$ ./fifo2 O_RDONLY &

[1] 152

Process 152 opening FIFO

$ ./fifo2 O_WRONLY

Process 153 opening FIFO

Process 152 result 3

Process 153 result 3

Process 152 finished

Process 153 finished

Это, наверное, самое распространенное применение именованных каналов. Оно позволяет читающему процессу стартовать и ждать в вызове open, а затем разрешает обеим программам продолжить выполнение, когда вторая программа откроет канал FIFO. Обратите внимание на то, что и читающий, и пишущий процессы были синхронизированы вызовом open.

Примечание

Когда процесс в ОС Linux заблокирован, он не потребляет ресурсы ЦП, поэтому этот метод синхронизации очень эффективен с точки зрения использования ЦП.

O_RDONLY с O_NONBLOCK и O_WRONLY

В следующем примере читающий процесс выполняет вызов open и немедленно продолжается, даже если нет ни одного пишущего процесса. Пишущий процесс тоже немедленно продолжает выполняться после вызова open, потому что канал FIFO уже открыт для чтения.

$ ./fifо2 O_RDONLY O_NONBLOCK &

[1] 160

Process 160 opening fifo

$ ./fifo2 O_WRONLY

Process 161 opening FIFO

Process 160 result 3

Process 161 result 3

Process 160 finished

Process 161 finished

[1]+ Done   ./fifo2 O_RDONLY O_NONBLOCK

Эти два примера — вероятно, самые распространенные комбинации режимов open. Не стесняйтесь использовать программу-пример для экспериментов с другими возможными комбинациями.

Чтение из каналов FIFO и запись в них

Применение режима O_NONBLOCK влияет на поведение вызовов read и write в каналах FIFO.

Вызов read, применяемый для чтения из пустого блокирующего FIFO (открытого без флага O_NONBLOCK), будет ждать до тех пор, пока не появятся данные, которые можно прочесть. Вызов read, применяемый в неблокирующем FIFO, напротив, при отсутствии данных вернет 0 байтов.

Вызов write для записи в полностью блокирующий канал FIFO будет ждать до тех пор, пока данные не смогут быть записаны. Вызов write, применяемый к FIFO, который не может принять все байты, предназначенные для записи, либо:

□ будет аварийно завершен, если был запрос на запись PIPE_BUF байтов или меньше и данные не могут быть записаны;

□ запишет часть данных, если был запрос на запись более чем PIPE_BUF байтов, и вернет количество реально записанных байтов, которое может быть и 0.

Размер FIFO — очень важная характеристика. Существует накладываемый системой предел объема данных, которые могут быть в FIFO в любой момент времени. Он задается директивой #define PIPE_BUF, обычно находящейся в файле limits.h. В ОС Linux и многих других UNIX-подобных системах он обычно равен 4096 байт, но в некоторых системах может быть и 512 байт. Система гарантирует, что операции записи PIPE_BUF или меньшего количества байтов в канал FIFO, который был открыт O_WRONLY (т.е. блокирующий), запишут или все байты, или ни одного.

Несмотря на то, что этот предел не слишком важен в простом случае с одним записывающим каналом FIFO и одним читающим FIFO, очень распространено использование одного канала FIFO, позволяющего разным программам отправлять запросы к этому единственному каналу FIFO. Если несколько разных программ попытаются писать в FIFO в одно и то же время, жизненно важно, чтобы блоки данных из разных программ не перемежались друг с другом, т. е. каждая операция write должна быть "атомарной". Как это сделать?

Если вы ручаетесь, что все ваши запросы write адресованы блокирующему каналу FIFO и их размер меньше PIPE_BUF байтов, система гарантирует, что данные никогда не будут разделены. Вообще это неплохая идея — ограничить объем данных, передаваемых через FIFO блоком в PIPE_BUF байтов, если вы не используете единственный пишущий и единственный читающий процессы.

Выполните упражнение 13.12.

Упражнение 13.12. Связь процессов с помощью каналов FIFO

Для того чтобы увидеть, как несвязанные процессы могут общаться с помощью именованных каналов, вам понадобятся две отдельные программы fifo3.c и fifo4.c.

1. Первая программа — поставщик. Она создает канал, если требуется, и затем записывает в него данные как можно быстрее.

Примечание

Поскольку пример иллюстративный, нас не интересуют конкретные данные, и мы не беспокоимся об инициализации буфера, В обоих листингах затененные строки содержат изменения, внесенные в программу fifo2.c помимо удаления кода со всеми аргументами командной строки.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <fcntl.h>

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

#define FIFO_NAME "/tmp/my_fifo"

#define BUFFER_SIZE PIPE_BUF

#define TEN_MEG (1024 * 1024 * 10)

int main() {

 int pipe_fd;

 int res;

 int open_mode = O_WRONLY;

 int bytes_sent = 0;

 char buffer[BUFFER_SIZE + 1];

 if (access(FIFO_NAME, F_OK) == -1) {

  res = mkfifo(FIFO_NAME, 0777);

  if (res != 0) {

   fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);

   exit(EXIT_FAILURE);

  }

 }

 printf("Process %d opening FIFO O_WRONLY\n", getpid());

 pipe_fd = open(FIFO_NAME, open_name);

 printf("Process %d result %d\n", getpid(), pipe_fd);

 if (pipe_fd != -1) {

  while (bytes_sent < TEN_MEG) {

   res = write(pipe_fd, buffer, BUFFER_SIZE);

   if (res == -1) {

    fprintf(stderr, "Write error on pipe\n);

    exit(EXIT_FAILURE);

   }

   bytes_sent += res;

  }

  (void)close(pipe_fd);

 } else {

  exit(EXIT_FAILURE);

 }

 printf("Process %d finished\n", getpid());

 exit(EXIT_SUCCESS);

}

2. Вторая программа, потребитель, гораздо проще. Она читает и выбрасывает данные из канала FIFO.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <fcntl.h>

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

#define FIFO_NAME "/tmp/my_fifo"

#define BUFFER_SIZE PIPE_BUF

int main() {

 int pipe_fd;

 int res;

 int open_mode = O_RDONLY;

 char buffer[BUFFER_SIZE - 1];

 int bytes_read = 0;

 memset(buffer, '\0', sizeof(buffer));

 printf("Process %d opening FIFO O_RDONLY\n", getpid());

 pipe_fd = open(FIFO_NAME, open_mode); 

 printf("Prосеss %d result %d\n", getpid(), pipe_fd);

 if (pipe_fd != -1) {

  do {

   res = read(pipe_fd, buffer,BUFFER_SIZE);

   bytes_read += res;

  } while (res > 0);

  (void)close(pipe_fd);

 } else {

  exit(EXIT_FAILURE);

 }

 printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);

 exit(EXIT_SUCCESS);

}

Когда вы выполните эти программы одновременно, с использованием команды time для хронометража читающего процесса, то получите следующий (с некоторыми пропусками для краткости) вывод:

$ ./fifo3 &

[1] 375

Process 375 opening FIFO O_WRONLY

$ time ./fifo4

Process 377 opening FIFO O_RDONLY

Process 375 result 3

Process 377 result 3

Process 375 finished

Process 377 finished, 10485760 bytes read

real 0m0.053s

user 0m0.020s

sys  0m0.040s

[1]+ Done   ./fifo3

Как это работает

Обе программы применяют FIFO в режиме блокировки. Вы запускаете первой программу fifo3 (пишущий процесс/поставщик), которая блокируется, ожидая, когда читающий процесс откроет канал FIFO. Когда программа fifo4 (потребитель) запускается, пишущий процесс разблокируется и начинает записывать данные в канал. В это же время читающий процесс начинает считывать данные из канала.

Примечание

ОС Linux так организует планирование двух процессов, что они оба выполняются, когда могут, и заблокированы в противном случае. Следовательно, пишущий процесс блокируется, когда канал полон, а читающий — когда канал пуст.

Вывод команды time показывает, что читающему процессу потребовалось гораздо меньше одной десятой секунды для считывания 10 Мбайт данных в процесс. Это свидетельствует о том, что каналы, по крайней мере, их реализация в современных версиях Linux, могут быть эффективным средством обмена данными между программами.

Более сложная тема: применение каналов FIFO в клиент-серверных приложениях

Заканчивая обсуждение каналов FIFO, давайте рассмотрим возможность построения очень простого клиент-серверного приложения, применяющего именованные каналы. Вы хотите, чтобы один серверный процесс принимал запросы, обрабатывал их и возвращал результирующие данные запрашивающей стороне — клиенту.

Вам нужно разрешить множественным клиентским процессам отправлять данные серверу. Для простоты предположим, что данные, которые нужно обработать, можно разбить на блоки, каждый из которых меньше PIPE_BUF байтов. Конечно, реализовать такую систему можно разными способами, но мы рассмотрим только один, как иллюстрацию применения именованных каналов.

Поскольку сервер будет обрабатывать только один блок данных в каждый момент времени, кажется логичным создать один канал FIFO, который читается сервером и в который записывают всё клиенты. Если открыть FIFO в блокирующем режиме, сервер и клиенты будут при необходимости блокироваться.

Возвращать обработанные данные клиентам немного сложнее. Вам придется организовать второй канал для возвращаемых данных, один для каждого клиента. Если передавать идентификатор (PID) процесса-клиента в исходных данных, отправляемых на сервер, обе стороны смогут использовать его для генерации уникального имени канала с возвращаемыми данными.

Выполните упражнение 13.13.

Упражнение 13.13. Пример клиент-серверного приложения

1. Прежде всего, вам нужен заголовочный файл client.h, в котором определены данные, общие для серверных и клиентских программ. В приложение также для удобства включены требуемые системные заголовочные файлы.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <fcntl.h>

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

#define SERVER_FIFO_NAME "/tmp/serv_fifo"

#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"

#define BUFFER_SIZE 20

struct data_to_pass_st {

 pid_t client_pid;

 char some_data[BUFFER_SIZE - 1];

};

2. Теперь займемся серверной программой server.c. В этом разделе вы создаете и затем открываете канал сервера. Он задается в режиме "только для чтения" и с блокировкой. После засыпания (из демонстрационных соображений) сервер читает данные от клиента, у которого есть структура типа data_to_pass_st.

#include "client.h"

#include <ctype.h>

int main() {

 int server_fifo_fd, client fifo_fd;

 struct data_to_pass_st my_data;

 int read_res;

 char client_fifo[256];

 char *tmp_char_ptr;

 mkfifo(SERVER_FIFO_NAME, 0777);

 server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);

 if (server_fifo_fd == -1) {

  fprintf(stderr, "Server fifo failure\n");

  exit(EXIT_FAILURE);

 }

 sleep(10); /* для целей демонстрации разрешает клиентам создать очередь */

 do {

  read_res = read(server_fifo_fd, &my_data, sizeof(my_data));

  if (read res > 0) {

3. На следующем этапе вы выполняете некоторую обработку данных, только что полученных от клиента: преобразуете все символы в некоторых данных в прописные и соединяете CLIENT_FIFO_NAME с полученным идентификатором client_pid.

   tmp_char_ptr = my_data.some_data;

   while (*tmp_char_ptr) {

    *tmp_char_ptr = toupper(* tmp_char_ptr);

    tmp_char_ptr++;

   }

   sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);

4. Далее отправьте обработанные данные назад, открыв канал клиентской программы в режиме "только для записи" и с блокировкой. В заключение закройте серверный FIFO с помощью закрытия файла и отсоединения FIFO.

   client_fifo_fd = open(client_fifo, O_WRONLY);

   if (client_fifo_fd ! = -1) {

    write(client_fifo_fd, &my_data, sizeof(my_data));

    close(client_fifo_fd);

   }

  }

 } while (read_res > 0);

 close(server_fifo_fd);

 unlink(SERVER_FIFO_NAME);

 exit(EXIT_SUCCESS);

}

5. Далее приведена клиентская программа client.с. В первой части этой программы FIFO сервера, если он уже существует, открывается как файл. Далее программа получает идентификатор собственного процесса, который формирует некие данные, которые будут отправляться на сервер. Создается FIFO клиента, подготовленный для следующего раздела.

#include "client.h"

#include <ctype.h>

int main() {

 int server_fifo_fd, client_fifo_fd;

 struct data_to_pass_st my_data;

 int times_to_send;

 char client_fifo[256];

 server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);

 if (server_fifo_fd == -1) {

  fprintf (stderr, "Sorry, no server\n");

  exit(EXIT_FAILURE);

 }

 my_data.client_pid = getpid();

 sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);

 if (mkfifo(client_fifo, 0777) == -1) {

  fprintf(stderr, "Sorry, can't make %s\n", client_fifo);

  exit(EXIT_FAILURE);

 }

6. В каждом из пяти проходов цикла клиентские данные отправляются на сервер. Далее клиентский FIFO открывается (в режиме "только для чтения" с блокировкой) и данные считываются обратно. В конце серверный FIFO закрывается, а клиентский FIFO удаляется из файловой системы.

 for (times_to_send = 0; times_to_send < 5; times_to_send++) {

  sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);

  printf("%d sent %s, ", my_data.client_pid, my_data.some_data);

  write(server_fifo_fd, &my_data, sizeof(my_data));

  client_fifo_fd = open(client_fifo, O_RDONLY);

  if (client_fifo_fd != -1) {

   if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) {

    printf("received: %s\n", my_data.some_data);

   }

   close(client_fifo_fd);

  }

 }

 close(server_fifo_fd);

 unlink(client_fifo);

 exit(EXIT_SUCCESS);

}

Для тестирования этого приложения вам необходимо запустить единственную копию сервера и несколько клиентов. Для того чтобы запустить их приблизительно в одно и то же время, примените следующие команды командной оболочки.

$ ./server &

$ for i in 1 2 3 4 5

do

./client &

done

$

Они запускают один серверный процесс и пять клиентских. Вывод клиентских программ, отредактированный для краткости, выглядит следующим образом:

531 sent Hello from 531, received: HELLO FROM 531

532 sent Hello from 532, received: HELLO FROM 532

529 sent Hello from 529, received: HELLO FROM 529

530 sent Hello from 530, received: HELLO FROM 530

531 sent Hello from 531, received: HELLO FROM 531

532 sent Hello from 532, received: HELLO FROM 532

Как видно из данного вывода, запросы разных клиентов перемежаются, но каждый клиент получает соответствующим образом обработанные и возвращаемые ему данные. Имейте в виду, что вы можете увидеть или не увидеть чередование запросов, т.к. порядок получения клиентских запросов может меняться от машины к машине и даже в разных сеансах работы приложения на одной машине.

Как это работает

Теперь мы обсудим последовательность клиентских и серверных операций во взаимодействии, чего не делали до сих пор.

Сервер создает свой канал FIFO в режиме "только чтение" и блокируется. Он делает это до тех пор, пока первый клиентский процесс не подсоединится, открыв тот же FIFO для записи. В этот момент серверный процесс разблокируется и выполняется вызов sleep, поэтому вызовы write клиентов образуют очередь. (В реальном приложении вызов sleep может быть удален, мы применяем его только чтобы продемонстрировать корректное функционирование программы с множественными одновременно действующими клиентами.)

Между тем, после того как клиентский процесс открыл серверный канал FIFO, он создает собственный FIFO с уникальным именем для считывания данных с сервера. Только после этого клиент записывает данные на сервер (причем, если канал полон или сервер все еще спит, клиентская программа блокируется) и затем блокирует для вызова read свой собственный канал FIFO, ожидая ответа.

Получив данные от клиента, сервер обрабатывает их, открывает клиентский канал для записи и записывает в него данные, что снимает блокировку клиентского процесса. Когда клиент разблокирован, он может читать из своего канала данные, записанные туда сервером.

Процесс повторяется полностью до тех пор, пока последний клиент не закроет канал сервера, вызывая аварийное завершение серверного вызова read (возвращение 0), поскольку ни у одного процесса нет серверного канала, открытого для записи. Если бы это был реальный серверный процесс, вынужденный ожидать будущих клиентов, возможно, вам пришлось бы изменить его, выбрав одно из двух:

□ открыть файловый дескриптор собственного серверного канала, чтобы вызов read всегда его блокировал, а не возвращал 0;

□ закрыть и повторно открыть серверный канал, когда read вернет 0 байтов, чтобы серверный процесс блокировался вызовом open, ожидая клиента, так, как он это делал, стартуя первый раз.

Оба эти метода проиллюстрированы в новом варианте приложения для работы с базой данных компакт-дисков, использующем именованные каналы.

Приложение для работы с базой данных компакт-дисков

Теперь, зная, как применять именованные каналы для реализации простой клиент-серверной системы, вы можете пересмотреть приложение для работы с базой данных компакт-дисков и соответствующим образом переработать его. Вы включите в него также некоторую обработку сигналов, позволяющую выполнить кое-какие действия по наведению порядка при прерывании процесса. Будет использоваться более ранняя версия приложения с dbm и интерфейсом командной строки, чтобы исходный текст программы был максимально простым и понятным.

Прежде чем подробно рассматривать эту новую версию, необходимо откомпилировать приложение. Если вы взяли исходный код с Web-сайта, примените make-файл для его компиляции и получения серверной и клиентской программ.

Примечание

Как было показано ранее в главе 7, в различных дистрибутивах файлы dbm именуются и устанавливаются немного по-разному. Если предоставленные файлы не компилируются в вашем дистрибутиве, вернитесь к главе 7 и поищите сведения об именах и местонахождении файлов dbm.

Выполнение команды server -i позволяет программе инициализировать новую базу данных компакт-дисков.

Нет нужды говорить о том, что клиент не выполнится, пока сервер не установится и не запустится. Далее приведен make-файл, показывающий, как совмещаются программы:

all: server client

CC=cc

CFLAGS= -pedantic -Wall

# Для отладки удалите знак комментария в следующей строке

# DFLAGS=-DDEBUG_TRACE=1 -g

# Где и какую версию dbm мы применяем.

# Предполагается, что gdbm предустановлена в стандартном месте, но мы

# собираемся применять подпрограммы, совместимые с gdbrn, которые

# заставляют ее эмулировать ndbm. Делается это потому, что ndbm — 'самая

# стандартная' из версий dbm. Возможно, вам потребуется внести изменения

# в соответствии с вашим дистрибутивом.

DBM_INC_PATH=/usr/include/gdbm

DBM_LIB_PATH=/usr/lib

DBM_LIB_FILE=-lgdbm

# В некоторых дистрибутивах может понадобиться изменить предыдущую

# строку, чтобы включить библиотеку совместимости, как показано далее.

# DBM_LIB_FILE=-lgdbm_compat -lgdbm

.с.о:

 $(CC) $(CFLAGS) -I$(DBM_INC_PATH) $(DFLAGS) -с $<

app_ui.o: app_ui.c cd_data.h

cd_dbm.o: cd_dbm.c cd_data.h

client_f.o: client_f.c cd_data.h cliserv.h

pipe_imp.o: pipe_imp.c cd_data.h cliserv.h

server.о: server.с cd_data.h cliserv.h

client: app_ui.o clientif.o pipe_imp.o

 $(CC) -o client $(DFLAGS) app_ui.о clientif.o pipe_imp.o

server: server.о cd_dbm.o pipe_imp.o

 $(CC) -o server -L$(DBM_LIB_PATH) $(DFLAGS) server.о cd_dbm.o pipe_imp.o -l$(DBM_LIB_FILE)

clean:

 rm -f server client_app *.o *~

Цели

Наша задача — отделить часть приложения, работающую с базой данных, от пользовательского интерфейса приложения. Вам также необходимо выполнять один серверный процесс, но разрешить одновременное выполнение множества клиентских процессов и при этом сократить до минимума изменения, вносимые в существующий программный код. Везде, где это возможно, вы сохраните исходный текст приложения неизменным.

Для простоты у вас должна быть возможность создавать (и удалять) каналы внутри приложения, не заставляя администратора системы создавать именованные каналы перед тем, как вы сможете их применять.

Важно также не использовать состояние "активного ожидания", чтобы не тратить времени ЦП на ожидание события. Как вы видели, ОС Linux позволяет приостанавливать выполнение в ожидании событий без потребления значительных ресурсов. Следует применять блокирующие свойства каналов для гарантии эффективного использования ЦП. В конце концов, теоретически сервер может ждать в течение многих часов поступления запроса.

Реализация

В предыдущей версии приложения, реализованного в виде единого процесса, с которой вы познакомились в главе 7, для управления данными применялся набор подпрограмм доступа к данным. К ним относились следующие подпрограммы:

int database_initialize(const int new_database);

void database_close(void);

cdc_entry get_cdc_entry(const char *cd_catalog_ptr);

cdt_entry get_cdt_entry(const char *cd_catalog_ptr, const int track_no);

int add_cdc_entry(const cdc_entry entry_to_add);

int add_cdt_entry(const cdt_entry entry_to_add);

int del_cdc_entry(const char *cd_catalog_ptr);

int del_cdt_entry(const char *cd_catalog_ptr, const int track_no);

cdc_entry search_cdc_entry(const char *cd_catalog_ptr,

 int *first_call_ptr);

В этих функциях очень удобно провести резкую границу между клиентом и сервером.

В реализации в виде единого процесса вы можете разделить приложение на две части (рис. 13.6), несмотря на то, что оно компилировалось как единая программа.

Рис. 13.6 

В клиент-серверную версию приложения вы хотите включить несколько именованных каналов и сопроводительный программный код для связи двух основных частей приложения. На рис. 13.7 показана необходимая структура.

Рис. 13.7 

В данной реализации подпрограммы интерфейса и клиента, и сервера помещены в один файл pipe_imp.c. Это сохраняет в едином файле весь программный код, зависящий от применения именованных каналов в клиент-серверной реализации. Форматирование и упаковка передаваемых данных хранятся отдельно от подпрограмм, реализующих именованные каналы. В результате у вас появятся дополнительные файлы исходного текста программы, но с более логичным разделением. Структура вызовов в приложении показана на рис. 13.8.

Рис. 13.8 

Файлы арр_ui.c, client_if.c и pipe_imp.c компилируются и компонуются вместе для получения клиентской программы. Файлы cd_dbm.c, server.c и pipe_imp.c компилируются и компонуются вместе для создания серверной программы. Заголовочный файл cliserv.h действует как заголовочный файл общих определений для связывания обеих программ.

В файлы app_ui.c и cd_dbm.c внесены очень незначительные изменения, в принципе позволяющие разделить приложение на две программы. Поскольку теперь приложение очень большое и существенная часть программного кода не изменилась по сравнению с предыдущей версией, здесь мы покажем только файлы cliserv.h, сlient_if.c и pipe_imp.c.

Заголовочный файл cliserv.h

Сначала рассмотрим cliserv.h. Этот файл определяет клиент-серверный интерфейс. Он необходим и клиентской, и серверной программам.

1. Далее приведены необходимые директивы #include.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

2. Затем вы определяете именованные каналы. Используйте один канал для сервера и по одному каналу для каждого клиента. Поскольку клиентов может быть несколько, клиентская программа включает идентификатор процесса в имя, таким образом, обеспечивая уникальность канала.

#define SERVER_PIPE "/tmp/server_pipe"

#define CLIENT_PIPE "/tmp/client_%d_pipe"

#define ERR_TEXT_LEN 80

3. Реализуйте команды как перечислимые типы, а не как директивы #define.

Примечание

Это хорошая возможность для компилятора выполнить дополнительную проверку типов и помочь в отладке приложения, т.к. многие отладчики могут показывать имена перечислимых констант, но не имена, определенные директивой #define.

Первый оператор typedef задает тип запроса, отправляемого на сервер; второй описывает тип серверного ответа клиенту.

typedef enum {

 s_create_new_database = 0,

 s_get_cdc_entry,

 s_get_cdt_entry,

 s_add_cdc_entry,

 s_add_cdt_entry,

 s_del_cdc_entry,

 s_del_cdt_entry,

 s_fmd_cdc_entry

} client_request_e;

typedef enum {

 r_success = 0,

 r_failure,

 r_find_no_more

} server_response_e;

4. Далее объявите структуру, которая будет формировать сообщение, передаваемое между двумя процессами в обоих направлениях.

Примечание

Поскольку на самом деле вам не нужно возвращать cdc_entry и cdt_entry в одном ответе, вы могли бы сделать их объединением (union). Но для простоты можно оставить их отдельными элементами, кроме того, в этом случае легче поддерживать программный код.

typedef struct {

 pid_t client_pid;

 client_request_e request;

 server_response_e response;

 cdc_entry cdc_entry_data;

 cdt_entry cdt_entry_data;

 char error_text[ERR_TEXT_LEN + 1];

} message_db_t;

5. В заключение приведены функции интерфейса канала, выполняющие передачу данных и содержащиеся в файле pipe_imp.c. Они делятся на функции серверной и клиентской стороны, в первом и втором блоках соответственно.

int server_starting(void);

void server_ending(void);

int read_request_from_client(message_db_t *rec_ptr);

int start_resp_to_client(const message_db_t mess_to_send);

int send_resp_to_client(const message_db_t mess_to_send);

void end_resp_to_client(void);

int client_starting(void);

void client_ending(void);

int send_mess_to_server(message_db_t mess_to_send);

int start_resp_from_server(void);

int read_resp_from_server(message_db_t *rec_ptr);

void end_resp_from_server(void);

Мы разделим последующее обсуждение на функции клиентского интерфейса и детали серверных и клиентских функций, хранящихся в файле pipe_imp.c, и при необходимости будем обращаться к исходному программному коду.

Функции интерфейса клиента

Рассмотрим файл clientif.c. Он предоставляет "поддельные" версии подпрограмм доступа к базе данных. Они кодируют запрос в структуре message_db_t и затем применяют подпрограммы из файла pipe_imp.c для передачи запроса серверу. Такой подход позволит вам внести минимальные изменения в первоначальный файл app_ui.c.

Интерпретатор клиента

1. В этом файле реализовано девять функций для работы с базой данных, объявленных в файле cd_data.h. Делает он это передачей запросов серверу и затем возвратом ответа сервера из функции, действуя как посредник. Файл начинается с файлов #include и констант.

#define _POSIX_SOURCE

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

#include "cd_data.h"

#include "cliserv.h"

2. Статическая переменная mypid уменьшает количество вызовов getpid, требуемых в противном случае. Мы применяем локальную функцию read_one_response для устранения дублирующегося программного кода.

static pid_t mypid;

static int read_one_response(message_db_t *rec_ptr);

3. Подпрограммы database_initialize и close все еще вызываются, но теперь используются, соответственно, для инициализации клиентского интерфейса каналов и удаления лишних именованных каналов, когда клиент завершил выполнение.

int database_initialize(const int new_database) {

 if (!client_starting()) return(0);

 mypid = getpid();

 return(1);

}

/* инициализация базы данных */

void database_close(void) {

 client_ending();

}

4. Подпрограмма get_cdc_entry вызывается для получения элемента каталога из базы данных по заданному названию компакт-диска в каталоге. В ней вы кодируете запрос в структуре message_db_t и передаете его на сервер. Далее вы считываете обратно ответ в другую структуру типа message_db_t. Если элемент найден, он включается в структуру message_db_t как структура типа cdc_entry, поэтому вы можете передать соответствующую часть структуры.

cdc_entry get_cdc_entry(const char *cd_catalog_ptr) {

 cdc_entry ret_val;

 message_db_t mess_send;

 message_db_t mess_ret;

 ret_val.catalog[0] = '\0';

 mess_send.client_pid = mypid;

 mess_send.request = s_get_cdc_entry;

 strcpy(mess_send.cdc_entry_data.catalog, cd_catalog_ptr);

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    ret_val = mess_ret.cdc_entry_data;

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(ret_val);

}

5. Далее приведен исходный текст функции read_one_response, которая используется для устранения дублирующегося программного кода.

static int read_one_response(message_db_t *rec_ptr) {

 int return_code = 0;

 if (!rec_ptr) return(0);

 if (start_resp_from_server()) {

  if (read_resp_from_server(rec_ptr)) {

   return_code = 1;

  }

  end_resp_from_server();

 }

 return(return_code);

}

6. Остальные подпрограммы get_xxx, del_xxx и add_xxx реализованы аналогично функции get_cdc_entry и приводятся здесь для полноты картины. Сначала функция для извлечения дорожек компакт-диска.

cdt_entry get_cdt_entry(const char *cd_catalog_ptr,

 const int track no) {

 cdt_entry ret_val;

 message_db_t mess_send;

 message_db_t mess_ret;

 ret_val.catalog[0] = '\0';

 mess_send.client_pid = mypid; mess_send.request = s_get_cdt_entry;

 strcpy(mess_send.cdt_entry_data.catalog, cd_catalog_ptr);

 mess_send.cdt_entry_data.track_no = track_no;

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    ret_val = mess_ret.cdt_entry_data;

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(ret_val);

}

7. Далее две функции для вставки данных, первая для добавления элемента каталога, а вторая — дорожек в базу данных.

int add_cdc_entry(const cdc_entry entry_to_add) {

 message_db_t mess_send;

 message_db_t mess_ret;

 mess_send.client_pid = mypid;

 mess_send.request = s_add_cdc_entry;

 mess_send.cdc_entry_data = entry_to_add;

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    return(1);

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(0);

}

int add_cdt_entry(const cdt_entry entry_to_add) {

 message_db_t mess_send;

 message_db_t mess_ret;

 mess_send.client_pid = mypid;

 mess_send.request = s_add_cdt_entry;

 mess send.cdt_entry data = entry_to_add;

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    return(1);

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(0);

}

8. В заключение две функции для удаления данных.

int del_cdc_entry(const char *cd_catalog_ptr) {

 message_db_t mess_send;

 message_db_t mess_ret;

 mess_send.client_pid = mypid;

 mess_send.request = s_del_cdc_entry;

 strcpy(mess_send.cdc_entry_data.catalog, cd_catalog_ptr);

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    return(1);

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(0);

}

int del_cdt_entry(const char *cd_catalog_ptr, const int track no) {

 message_db_t mess_send;

 message_db_t mess_ret;

 mess_send.client_pid = mypid;

 mess_send.request = s_del_cdt_entry;

 strcpy(mess_send.cdt_entry_data.catalog, cd_catalog_ptr);

 mess_send.cdt_entry_data.track_no = track_no;

 if (send_mess_to_server(mess_send)) {

  if (read_one_response(&mess_ret)) {

   if (mess_ret.response == r_success) {

    return(1);

   } else {

    fprintf(stderr, "%s", mess_ret.error_text);

   }

  } else {

   fprintf(stderr, "Server failed to respond\n");

  }

 } else {

  fprintf(stderr, "Server not accepting requests\n");

 }

 return(0);

}

Поиск в базе данных

Функция поиска по ключу компакт-диска сложнее. Пользователь этой функции рассчитывает вызвать ее один раз для начала поиска. Мы удовлетворили его ожидания в главе 7, задавая параметр *first_call_ptr равным true при первом вызове функции, и функция в этом случае возвращает первое найденное совпадение. При последующих вызовах функции поиска указатель *first_call_ptr равен false и возвращаются дальнейшие совпадения, по одному на каждый вызов.

Теперь, когда вы разделили приложение на два процесса, нельзя разрешать поиску обрабатывать по одному элементу на сервере, потому что другой клиент может запросить у сервера иной поиск, когда выполняется ваш поиск. Вы не можете заставить серверную часть хранить отдельно содержимое (как далеко продвинулся поиск) для поиска каждого клиента, т.к. клиент может просто остановить поиск на полпути, когда найден нужный компакт-диск или клиент "упал".

Можно либо изменить алгоритм поиска, либо, как показано в приведенном далее программном коде, спрятать сложность в подпрограмме интерфейса. Данный код вынуждает сервер возвращать все возможные совпадения с искомым значением и затем сохраняет их во временном файле до тех пор, пока клиент не запросит их.

1. Эта функция не так сложна, как кажется, просто в ней вызываются три функции канала send_mess_to_server, start_resp_from_server и read_resp_fromserver, которые будут рассмотрены в следующем разделе.

cdc_entry search_cdc_entry(const char *cd_catalog_ptr,

 int *first_call_ptr) {

 message_db_t mess_send;

 message_db_t mess_ret;

 static FILE *work_file = (FILE *)0;

 static int entries_matching = 0;

 cdc_entry ret_val;

 ret_val.catalog[0] = '\0';

 if (!work_file && (*first_call_ptr == 0)) return(ret_val);

2. Далее показан первый вызов для поиска с указателем *first_call_ptr, равным true. Он немедленно приравнивается false, на случай, если вы забыли. Создается временный файл work_file и инициализируется структура сообщения клиенту.

if (*first_call_ptr) {

 *first_call_ptr = 0;

 if (work_file) fclose(work_file);

 work_file = tmpfile();

 if (!work_file) return(ret_val);

 mess_send.client_pid = mypid;

 mess_send.request = s_find_cdc_entry;

 strcpy(mess_send.cdc_entry_data.catalog, cd_catalog_ptr);

3. Теперь приводится проверка условий с тремя уровнями вложенности, заставляющая вызывать функции из файла pipe_imp.c. Если сообщение успешно отправлено на сервер, клиент ждет ответа от сервера. Пока считывания с сервера успешны, совпадения с искомой величиной возвращаются в work_file клиента и наращивается счетчик entries_matching.

 if (send_mess_to_server(mess_send)) {

  if (start_resp_from_server()) {

   while (read_resp_from_server(&mess_ret)) {

    if (mess_ret.response == r_success) {

     fwrite(&mess_ret.cdc_entry_data, sizeof(cdc_entry), 1, work_file);

     entries_matching++;

    } else {

     break;

    }

   } /* while */

  } else {

   fprintf(stderr, "Server not responding\n");

  }

 } else {

  fprintf (stderr, "Server not accepting requests\n");

 }

4. Следующая проверка ищет, есть ли совпадения с заданным значением. Далее вызов fseek переводит указатель в файле work_file на место записи следующей порции данных.

 if (entries_matching == 0) {

  fclose(work_file);

  work_file = (FILE *)0;

  return(ret_val);

 }

 (void)fseek(work_file, 0L, SEEK_SET);

5. Если это не первый вызов функции поиска для данного конкретного элемента, программа проверяет, были ли уже найдены совпадения. В заключение в структуру ret_val читается следующий совпадающий элемент. Предшествующие проверки гарантируют наличие совпадающего элемента.

 } else {

  /* не *first_call_ptr */

  if (entries_matching == 0) {

   fclose(work_file);

   work_file = (FILE *)0;

   return(ret_val);

  }

 }

 fread(&ret_val, sizeof(cdc_entry), 1, work_file);

 entries_matching--;

 return(ret_val);

}

Интерфейс сервера server.c

Если у клиента есть интерфейс для обращения к программе app_ui.c, серверу также нужна программа для управления (переименованной) программой cd_access.c, теперь cd_dbm.c. Далее приведена функция main сервера.

1. Начните с объявления нескольких глобальных переменных, прототипа функции process_command и функции перехвата сигнала для обеспечения чистого завершения.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <fcntl.h>

#include <limits.h>

#include <signal.h>

#include <string.h>

#include <errno.h>

#include <sys/types.h>

#include <sys/stat.h>

#include "cd_data.h"

#include "cliserv.h"

int save errno;

static int server_running = 1;

static void process_command(const message_db_t mess_command);

void catch_signals() {

 server_running = 0;

}

2. Теперь переходите к функции main. После проверки успешного завершения подпрограмм захвата сигнала программа проверяет, передали ли вы -i в командной строке. Если да, она создаст новую базу данных. Если вызов подпрограммы database_initialize в файле cd_dbm.c завершится аварийно, будет выведено сообщение об ошибке. Если все хорошо и сервер работает, любые запросы от клиента направляются функции process_command, которую вы вскоре увидите.

int main(int argc, char *argv[]) {

 struct sigaction new_action, old_action;

 message_db_t mess command;

 int database_init_type = 0;

 new_action.sa_handler = catch_signals;

 sigemptyset(&new_action.sa_mask);

 new_action.sa_flags = 0;

 if ((sigaction(SIGINT, &new_action, &old_action) != 0) ||

  (sigaction(SIGHUP, &new_action, &old_action) != 0) ||

  (sigaction(SIGTERM, &new_action, &old_action) != 0)) {

  fprintf(stderr, "Server startup error, signal catching failed\n");

  exit(EXIT_FAILURE);

 }

 if (argc > 1) {

  argv++;

  if (strncmp("-i", *argv, 2) == 0) database_init_type = 1;

 }

 if (!database_initialize(database_init_type)) {

  fprintf(stderr, "Server error :-\

   could not initialize database\n");

  exit (EXIT_FAILURE);

 }

 if (!server starting()) exit(EXIT_FAILURE);

 while(server_running) {

  if (read_request_from_client(&mess_command)) {

   process_command(mess_command);

  } else {

   if (server_running) fprintf(stderr,

    "Server ended — can not read pipe\n");

   server_running = 0;

  }

 } /* while */

 server_ending();

 exit(EXIT_SUCCESS);

}

3. Любые сообщения клиентов направляются в функцию process_command, где они обрабатываются в операторе case, который выполняет соответствующие вызовы из файла cd_dbm.c.

static void process_command(const message_db_t comm) {

 message_db_t resp;

 int first_time = 1;

 resp = comm; /* копирует команду обратно,

                 затем изменяет resp, как требовалось */

 if (!start_resp_to_client(resp)) {

  fprintf(stderr, "Server Warning:

   start_resp_to_client %d failed\n", resp.client_pid);

  return;

 }

 resp.response = r_success;

 memset(resp.error_text, '\0', sizeof(resp.error_text));

 save_errno = 0;

 switch(resp.request) {

 case s_create_new_database:

  if (!database initialize(1))

   resp.response = r_failure;

  break;

 case s_get_cdc_entry:

  resp.cdc_entry_data =

   get_cdc_entry(comm.cdc_entry_data.catalog);

  break;

 case s_get_cdt_entry:

  resp.cdt_entry_data =

   get_cdt_entry(comm.cdt_entry_data.catalog,

   comm.cdt_entry_data.track_no);

  break;

 case s_add_cdc_entry:

  if (!add_cdc_entry(comm.cdc_entry_data))

   resp.response = r_failure;

  break;

 case s_add_cdt_entry:

  if (!add_cdt_entry(comm.cdt_entry_data))

   resp.response = r_failure;

  break;

 case s_del_cdc_entry:

  if (!del_cdc_entry(comm.cdc_entry_data.catalog))

   resp.response = r_failure;

  break;

 case s_del_cdt_entry:

  if (!del_cdt_entry(comm.cdt_entry_data.catalog,

   comm.cdt_entry_data.track_no)) resp.response = r_failure;

  break;

 case s_find_cdc_entry:

  do {

   resp.cdc_entry_data =

    search_cdc_entry(comm.cdc_entry_data.catalog, &first_time);

   if (resp.cdc_entry_data.catalog[0] != 0) {

    resp.response = r_success;

    if (!send_resp_to_client(resp)) {

     fprintf(stderr,

      "Server Warning:- failed to respond to %d\n", resp.client_pid);

     break;

    }

   } else {

    resp.response = r_find_no_more;

   }

  } while (resp.response == r_success);

  break;

 default:

  resp.response = r_failure;

  break;

 } /* switch */

 sprintf(resp.error_text,

  "Command failed:\n\t%s\n", strerror(save_errno));

 if (!send_resp_to_client(resp)) {

  fprintf(stderr,

   "Server Warning:- failed to respond to %d\n", resp.client_pid);

 }

 end_resp_to_client();

 return;

}

Прежде чем рассматривать действующую реализацию канала, давайте обсудим последовательность событий, которые должны произойти для передачи данных между клиентским и серверным процессами. На рис. 13.9 показан запуск обоих, и клиентского, и серверного, процессов, а также то, как они образуют петлю во время обработки команд и ответов.

В этой реализации ситуация немного сложнее, т.к. в запросе на поиск клиент передает серверу одну команду и затем ждет один или несколько ответов от сервера. Это усложняет программу, особенно клиентскую часть.

Рис. 13.9 

Канал

Далее показан файл реализации канала pipe_imp.с, в котором содержатся клиентские и серверные функции.

Примечание

Как вы видели в главе 10, может быть определено символическое имя DEBUG_TRACE для того, чтобы показать последовательность вызовов, в которых клиентский и серверный процессы передают друг другу сообщения.

Заголовочный файл для реализации канала

1. Прежде всего, директивы #include:

#include "cd_data.h"

#include "cliserv.h"

2. Вы также определяете в файле программы несколько значений, нужных вам в разных функциях:

static int server_fd = -1;

static pid_t mypid = 0;

static char client_pipe_name[PATH_MAX + 1] = {'\0'};

static int client_fd = -1;

static int client_write_fd = -1;

Функции серверной стороны

Далее нужно рассмотреть функции серверной стороны. В следующем разделе показаны функции, открывающие и закрывающие именованный канал и читающие сообщения от клиентов. В следующем за ним разделе приведен программный код, который открывает и закрывает клиентские каналы и пересылает по ним сообщения, основываясь на идентификаторе процесса, который клиент включает в свое сообщение.

Функции сервера

1. Подпрограмма server_starting создает именованный канал, из которого сервер будет считывать команды. Далее она открывает канал для чтения. Этот вызов open будет блокировать выполнение, пока клиент не откроет канал для записи. Используйте режим блокировки для того, чтобы сервер мог выполнить блокировку вызовов read в канале в ожидании отправляемых ему команд.

int server_starting(void) {

#if DEBUG_TRACE

 printf("%d server_starting()\n", getpid());

#endif

 unlink(SERVER_PIPE);

 if (mkfifo(SERVER_PIPE, 0777) == -1) {

  fprintf(stderr, "Server startup error, no FIFO created\n");

  return(0);

 }

 if ((server_fd = open(SERVER_PIPE, O_RDONLY)) == -1) {

  if (errno == EINTR) return(0);

  fprintf(stderr, "Server startup error, no FIFO opened\n");

  return(0);

 }

 return(1);

}

2. Когда сервер завершает работу, он удаляет именованный канал, для того чтобы клиенты могли установить, что нет действующего сервера.

void server_ending(void) {

#if DEBUG_TRACE

 printf("%d:- server_ending()\n", getpid());

#endif

 (void)close(server_fd);

 (void)unlink(SERVER_PIPE);

}

3. Функция read_request_from_client будет блокировать чтение в серверном канале до тех пор, пока клиент не запишет в него сообщение.

int read_request_from_client(message_db_t *rec_ptr) {

 int return_code = 0;

 int read_bytes;

#if DEBUG_TRACE

 printf("%d :- read_request_from_client()\n", getpid());

#endif

 if (server_fd != -1) {

  read_bytes = read(server_fd, rec_ptr, sizeof(*rec_ptr));

  ...

 }

 return(return_code);

}

4. В особом случае, когда ни у одного клиента нет канала, открытого для записи, вызов read вернет 0, т.е. он обнаружит EOF (метку конца файла). Затем сервер закроет канал и откроет его снова так, что канал блокируется до тех пор, пока клиент также не откроет канал. Выполняется то же, что и при первом запуске сервера; вы инициализировали сервер повторно. Вставьте этот программный код в предыдущую функцию. 

if (read_bytes == 0) {

 (void)close(server_fd);

 if ((server_fd = open(SERVER_PIPE, O_RDONLY)) == -1) {

  if (errno != EINTR) {

   fprintf(stderr, "Server error, FIFO open failed\n");

  }

  return(0);

 }

 read_bytes = read(server_fd, rec_ptr, sizeof(*rec_ptr));

}

if (read_bytes == sizeof(*rec_ptr)) return_code = 1;

Сервер — это единственный процесс, способный одновременно обслуживать множество клиентов. Поскольку каждый клиент применяет свой канал для получения ответов, адресованных ему, сервер, для того чтобы отправить ответы разным клиентам, должен писать в разные каналы. Поскольку файловые дескрипторы — это ограниченный ресурс, сервер открывает клиентский канал для записи только тогда, когда у него есть данные для отправки.

В программном коде открытие клиентского канала, запись в него и закрытие канала разделены на три отдельные функции. Когда вы возвращаете многочисленные результаты поиска, такой подход необходим, для того чтобы можно было открыть канал один раз, записать в него множество ответов и затем снова закрыть канал.

Прокладка каналов

1. Сначала откройте канал клиента.

int start_resp_to_client(const message_db_t mess_to_send) {

#if DEBUG_TRACE

 printf("%d :- start_resp_to_client()\n", getpid());

#endif

 (void)sprintf(client_pipe_name, CLIENT_PIPE,

  mess_to_send.client_pid);

 if ((client_fd = open(client_pipe_name, O_WRONLY)) == -1) return(0);

 return(1);

}

2. Все сообщения отправляются с помощью данной функции. Соответствующие клиентские функции, которые принимают сообщение, вы увидите позже.

int send_resp_to_client(const message_db_t mess_to_send) {

 int write_bytes;

#if DEBUG_TRACE

 printf("%d :- send_resp_to_client()\n", getpid());

#endif

 if (client_fd == -1) return(0);

 write_bytes = write(client_fd, &mess_to_send, sizeof(mess_to_send));

 if (write_bytes != sizeof(mess_to_send)) return(0);

 return(1);

}

3. В заключение закройте клиентский канал.

void end resp_to_client(void) {

#if DEBUG_TFACE

 printf("%d :- end_resp_to_client()\n", getpid());

#endif

 if (client_fd != -1) {

  (void)close(сlient_fd);

  client_fd = -1;

 }

}

Функции на стороне клиента

Дополнение к серверу — клиентские функции в файле pipe_imp.c. Они очень похожи на серверные функции за исключением функции с интригующим именем send_mess_to_server.

Клиентские функции

1. После проверки доступности сервера функция client_starting инициализирует канал клиентской стороны.

int client_starting(void) {

#if DEBUG_TFACE

 printf("%d client_starting\n", getpid());

#endif

 mypid = getpid();

 if ((server_fd = open(SERVER_PIPE, O_WRONLY)) == -1) {

  fprintf(stderr, "Server not running\n");

  return(0);

 }

 (void)sprintf(client pipe name, CLIENT_PIPE, mypid);

 (void)unlink(client_pipe_name);

 if (mkfifo(client_pipe_name, 0777) == -1) {

  fprintf(stderr, "Unable to create client pipe %s\n", client_pipe_name);

  return(0);

 }

 return(1);

}

2. Функция client_ending закрывает файловые дескрипторы и удаляет ненужный теперь именованный канал.

void client_ending(void) {

#if DEBUG_TRACE

 printf("%d client_ending()\n", getpid());

#endif

 if (client_write_fd != -1) (void)close(client_write_fd);

 if (client_fd != -1) (void)close(client_fd);

 if (server_fd != -1) (void)close(server_fd);

 (void)unlink(client_pipe_name);

}

3. Функция send_mess_to_server передает запрос через канал сервера.

int send_mess_to_server(message_db_t mess_to_send) {

 int write_bytes;

#if DEBUG_TRACE

 printf("%d send_mess_to_server()\n", getpid());

#endif

 if (server_fd == -1) return(0);

 mess_to_send.client_pid = mypid;

 write_bytes = write(server_fd, &mess_to_send, sizeof(mess_to_send));

 if (write_bytes != sizeof(mess_to_send)) return(0);

 return(1);

}

Как и в серверных функциях, клиент получает назад результаты от сервера с помощью трех функций, обслуживающих множественные результаты поисков.

Получение результатов с сервера

1. Данная клиентская функция запускается для ожидания ответа сервера. Она открывает канал клиента только для чтения и затем повторно открывает файл канала только для записи. Чуть позже в этом разделе вы поймете почему.

int start_resp_from_server(void) {

#if DEBUG_TRACE

 printf("%d :- start_resp_from_server()\n", getpid());

#endif

 if (client_pipe_name[0] == '\0') return(0);

 if (client_fd != -1) return(1);

 client_fd = open(client_pipe_name, O_RDONLY);

 if (client_fd != -1) {

  client_write_fd = open(client_pipe_name, O_WRONLY);

  if (client_write_fd != -1) return(1);

  (void)close(client_fd);

  client_fd = -1;

 }

 return(0);

}

2. Далее приведена основная операция read, которая получает с сервера совпадающие элементы базы данных.

int read_resp_from_server(message_db_t *rec_ptr) {

 int read_bytes;

 int return_code = 0;

#if DEBUG_TRACE

 printf("%d :- reader_resp_from_server()\n", getpid());

 #endif

 if (!rec_ptr) return(0);

 if (client_fd = -1) return(0);

 read_bytes = read(client_fd, rec_ptr, sizeof(*rec_ptr));

 if (read_bytes = sizeof(*rec_ptr)) return_code = 1;

 return(return_code);

}

3. И в заключение приведена клиентская функция, помечающая конец ответа сервера.

void end_resp_from_server(void) {

#if DEBUG_TRACE

 printf("%d :- end_resp_from_server()\n", getpid());

#endif

 /* В реализации канала эта функция пустая */

}

Второй дополнительный вызов open канала клиента для записи в start_resp_from_server

client_write_fd = open(client_pipe_name, O_WRONLY);

применяется для защиты от ситуации гонок, когда серверу необходимо быстро откликаться на несколько запросов клиента,

Для того чтобы стало понятнее, рассмотрим такую последовательность событий:

1. Клиент пишет запрос к серверу.

2. Сервер читает запрос, открывает канал клиента и отправляет обратно ответ, но приостанавливает выполнение до того, как успеет закрыть канал клиента.

3. Клиент открывает канал для чтения, читает первый ответ и закрывает свой канал.

4. Далее клиент посылает новую команду и открывает клиентский канал для чтения.

5. Сервер возобновляет работу, закрывая свой конец клиентского канала.

К сожалению, в этот момент клиент пытается считать из канала ответ на свой следующий запрос, но read вернет 0 байтов, поскольку ни один процесс не открыл клиентский канал для записи.

Разрешив клиенту открыть канал как для чтения, так и для записи, и устранив тем самым необходимость повторного открытия канала, вы избежите подобной ситуации гонок. Учтите, что клиент никогда не пишет в канал, поэтому нет опасности считывания ошибочных данных.

Резюме, касающееся приложения

Вы разделили приложение, управляющее базой данных компакт-дисков, на клиентскую и серверную части, что позволило разрабатывать независимо пользовательский интерфейс и внутреннюю технологию работы с базой данных. Как видите, четко определенный интерфейс базы данных дает возможность каждому важному элементу приложения наилучшим образом использовать машинные ресурсы. Если пойти чуть дальше, можно было бы заменить реализацию с помощью каналов на сетевой вариант и применить выделенный компьютер для сервера базы данных. В главе 15 вы узнаете больше об организации сети.

Резюме 

В этой главе вы рассмотрели передачу данных между процессами с помощью каналов. Сначала вы познакомились с неименованными каналами, которые создаются вызовом popen или pipe, и посмотрели, как, применяя канал и вызов dup, можно передать данные из одной программы в стандартный ввод другой. Далее вы перешли к именованным каналам и узнали, как можно передавать данные между несвязанными программами. В заключение вы реализовали простой пример клиент- серверного приложения, используя каналы FIFO для обеспечения не только синхронизации процессов, но и организации двунаправленного потока данных. 

Глава 14

Семафоры, совместно используемая память и очереди сообщений

В этой главе мы обсудим набор средств, обеспечивающих взаимодействие процессов и первоначально введенных в версии ОС UNIX AT&T System V.2. Поскольку все эти средства появились в одном выпуске системы и обладают одинаковым программным интерфейсом, их часто называют средствами IPC (Inter-Process Communication, взаимодействие между процессами) или более полно System V IPC. Как вы уже видели, это далеко не единственный способ установления связи между процессами, но термин "System V IPC" обычно применяется для обозначения именно этих конкретных средств.

В данной главе мы рассмотрим следующие темы:

□ семафоры для управления доступом к ресурсам;

□ совместно используемая память для эффективного использования общих данных разными программами;

□ обмен сообщениями как легкий способ передачи данных между программами.

Семафоры

Когда разрабатываются программы для многопользовательских или многозадачных систем или их комбинации, зачастую выясняется, что в программе есть важные разделы программного кода, в которых необходимо обеспечить единственному процессу (или одному потоку исполнения) монопольный доступ к ресурсу.

У семафоров сложный программный интерфейс. Но, к счастью, вы сможете предоставить существенно, упрощенный его вариант, достаточный для решения большинства проблем, требующих программирования семафоров. 

В первом приложении-примере в главе 7, использующем средство dbm для доступа к базе данных, данные могли бы быть повреждены множественными программами, пытавшимися обновить базу данных в одно и то же время. Никакого сбоя не произойдет, если две разные программы запрашивают у двух разных пользователей ввод данных для базы данных, единственная потенциальная проблема кроется в частях программного кода, обновляющих базу данных. Эти секции программы, действительно выполняющие обновления и нуждающиеся в монопольном режиме выполнения, называются критическими секциями. Часто они занимают всего несколько строк кода в гораздо больших по объему программах.

Для устранения проблем, вызванных одновременным обращением нескольких программ к совместно используемому ресурсу, вам нужен способ генерации и применения маркера, гарантирующего в любой момент, времени доступ в критическую секцию только одному потоку исполнения. В главе 12 вы вкратце познакомились с ориентированным на потоки использованием мьютексов или семафоров для управления доступом в критические секции многопоточной программы. В этой главе мы вернемся к теме семафоров, но акцентируем внимание на их применении для взаимодействия разных процессов.

Примечание

Функции семафоров, применяемые в потоках и обсуждавшиеся в главе 12, не относятся к наиболее общим функциям, которые мы рассматриваем в этой главе, поэтому будьте внимательны и не путайте функции этих двух типов.

Написать программный код общего назначения, который гарантирует одной программе монопольный доступ к конкретному ресурсу, на удивление сложно, несмотря на то, что существует решение, известное как алгоритм Деккера (Dekker's Algorithm). К сожалению, этот алгоритм полагается на состояние активного ожидания или спин-блокировки, в котором процесс выполняется непрерывно, ожидая изменения адреса памяти. В многозадачной среде, какой является ОС Linux, это нежелательные расходы ресурсов ЦПУ. Ситуация существенно облегчается, когда для обеспечения монопольного доступа есть аппаратная поддержка, обычно в виде специальных команд ЦПУ. Примером аппаратной поддержки могла бы быть команда обращения к ресурсу и приращения регистра атомарным образом, так чтобы никакая другая команда (даже прерывание) не могла появиться между операциями чтения/инкремента/записи.

Одним из возможных решений проблемы можно считать уже знакомое вам создание файла с помощью флага O_EXCL в функции open, обеспечивающей атомарное создание файла. Этот метод хорош для простых задач, но становится довольно путанным и очень неэффективным при решении более сложных примеров.

Важный шаг вперед в сфере параллельного программирования был сделан, когда голландский специалист в области компьютерных наук Эдсгер Дейкстра (Edsger Dijkstra) предложил идею семафоров. Как уже кратко упоминалось в главе 12, семафор — это специальная переменная, которая принимает только целые положительные значения и с помощью которой программы могут действовать только атомарно. В этой главе мы расширим данное ранее упрощенное определение. Будет более подробно рассказано, как действуют семафоры и как для взаимодействия отдельных процессов применяются функции общего назначения вместо особого случая многопоточных программ, которые рассматривались в главе 12.

Определяя более строго, семафор — это специальная переменная, для которой разрешены только две операции, формально именуемые ожиданием или приостановкой (wait) и оповещением (signal). Поскольку в программировании Linux у приостановки и оповещения уже есть специальные значения, мы будем применять оригинальное обозначение:

□ P(переменная-семафор) для приостановки (wait);

□ V(переменная-семафор) для оповещения (signal).

Эти буквы взяты из голландских слов для приостановки (passeren — проходить, пропускать как в случае контрольной точки перед критической секцией) и для оповещения (vrijgeven — предоставлять или освобождать, как в случае отказа от контроля критической секции). Вы можете встретить термины "вверх" (up) и "вниз" (down), применяемые в отношении семафоров по аналогии с использованием сигнальных флажков.

Описание семафора

Простейший семафор — это переменная, способная принимать только значения 0 и 1, бинарный или двоичный семафор. Это наиболее распространенный вид семафора. Семафоры, принимающие много положительных значений, называют семафорами общего вида. В оставшейся части главы мы сосредоточимся на двоичных семафорах.

Определения операций P и V удивительно просты. Предположим, что у вас есть переменная-семафор sv. В этом случае обе операции определяются так, как представлено в табл. 14.1.

Таблица 14.1

Операция Описание
Р(sv) Если sv больше нуля, она уменьшается на единицу. Если sv равна 0, выполнение данного процесса приостанавливается
V(sv) Если какой-то другой процесс был приостановлен в ожидании семафора sv, переменная заставляет его возобновить выполнение. Если ни один процесс не приостановлен в ожидании семафора sv, значение переменной увеличивается на единицу

Другой способ описания семафора — считать, что переменная sv, равная true, когда доступна критическая секция, уменьшается на единицу с помощью P(sv) и становится равна false, когда критическая секция занята, и увеличивается на единицу операцией V(sv), когда критическая секция снова доступна. Имейте в виду, что обычная переменная, которую вы уменьшаете и увеличиваете на единицу, не годится, т.к. в языках С, С++, C# или практически в любом традиционном языке программирования у вас нет возможности сформировать единую атомарную операцию, проверяющую, равна ли переменная true, и если это так, изменяющую ее значение на false. Именно эта функциональная возможность делает операции с семафором особенными.

Теоретический пример

С помощью простого теоретического примера можно посмотреть, как действует семафор. Предположим, что у вас есть два процесса: proc1 и proc2, оба нуждающиеся в некоторый момент выполнения в монопольном доступе к базе данных. Вы определяете один бинарный семафор sv, который стартует со значением 1 и доступен обоим процессам. Далее обоим процессам нужно выполнить одну и ту же обработку для доступа к критической секции программного кода; эти два процесса могут быть двумя разными выполняющимися экземплярами одной и той же программы.

Оба процесса совместно используют переменную-семафор sv. Как только один процесс выполнил операцию P(sv), он получил семафор и может войти в критическую секцию программы. Второму процессу вход в критическую секцию запрещен, т.к., когда он попытается выполнить операцию P(sv), он вынужден будет ждать до тех пор, пока первый процесс не покинет критическую секцию и не выполнит операцию V(sv), освобождающую семафор.

Требуемый псевдокод у обоих процессов идентичен:

semaphore sv = 1;

loop forever {

 P(sv);

 critical code section;

 V(sv);

 noncritical code section;

}

Код на удивление прост, потому что определение операций P и V наделяет их большими функциональными возможностями.

Рис. 14.1 

На рис. 14.1 показана схема действующих операций P и V, напоминающих ворота в критических секциях программного кода.

Реализация семафоров в Linux

Теперь, когда вы увидели, что такое семафоры и как они действуют в теории, можно рассмотреть, как их свойства реализованы в ОС Linux. Интерфейс тщательно проработан и предлагает гораздо больше возможностей, чем обычно требуется. Все функции семафоров в Linux оперируют массивами семафоров общего вида, а не одним двоичным семафором. На первый взгляд кажется, что такой подход все усложняет, но если процесс нуждается в блокировке нескольких ресурсов, способность оперировать массивом семафоров — большое подспорье. В этой главе мы сосредоточимся на применении одиночных семафоров, поскольку в большинстве случаев это все, что вам нужно.

Далее приведены объявления функций семафоров:

#include <sys/sem.h>

int semctl(int sem_id, int sem_num, int command, ...);

int semget(key_t key, int num_sems, int sem_flags);

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

Примечание

Обычно заголовочный файл sys/sem.h опирается на два других заголовочных файла: sys/types.h и sys/ipc.h. Как правило, они автоматически включаются в программу файлом sys/sem.h и вам не нужно задавать их явно в директивах #include.

Прорабатывая каждую функцию отдельно, помните о том, что все они спроектированы для использования массивов значений семафоров, что делает их работу существенно более сложной, чем та, что необходима для обработки одного семафора.

Обратите внимание на то, что параметр key действует во многом как имя файла, т.к. он тоже представляет ресурс, который программы могут использовать и кооперироваться при этом, если соблюдают соглашение об общем имени для него. Аналогичным образом идентификатор, возвращаемый функцией semget и применяемый другими функциями, совместно использующими память, очень похож на файловый поток FILE*, возвращаемый функцией fopen и представляющий собой значение, применяемое процессом для доступа к совместно используемому файлу. Как и в случае файлов, у разных процессов будут разные идентификаторы семафоров, несмотря на то, что они ссылаются на один и тот же семафор. Такое применение ключа и идентификаторов — общее для всех средств IPC, обсуждаемых здесь, несмотря на то, что каждое средство применяет независимые ключи и идентификаторы.

semget

Функция semget создает новый семафор или получает ключ существующего семафора.

int semget(key_t key, int num_sems, int sem_flags);

Первый параметр key — целочисленное значение, позволяющее несвязанным процессам обращаться к одному и тому же семафору. Ко всем семафорам осуществляется непрямой доступ с помощью программы, предоставляющей ключ, для которого система затем генерирует идентификатор семафора. Ключ семафора применяется только в функции semget. Все остальные функции семафора используют идентификатор семафора, возвращаемый функцией semget.

Существует особое значение ключа семафора IPC_PRIVATE, которое предназначено для создания семафора, доступ к которому получает только процесс-создатель, но такой семафор редко бывает полезен. Для создания нового семафора следует задавать уникальное ненулевое целое число.

Параметр num_sems определяет количество требуемых семафоров. Почти всегда он равен 1.

Параметр sem_flags — набор флагов, очень похожих на флаги функции open. Младшие девять байтов — права доступа к семафору, ведущие себя, как права доступа к файлу. Кроме того, для создания нового семафора с помощью поразрядной операции OR их можно объединить со значением IPC_CREAT. Не считается ошибкой наличие флага IPC_CREAT и задание ключа существующего семафора. Флаг IPC_CREAT безмолвно игнорируется, если в нем нет нужды. Можно применять флаги IPC_CREAT и IPC_EXCL для гарантированного получения нового уникального семафора. Если семафор уже существует, функция вернет ошибку.

Функция semget вернет в случае успеха положительное (ненулевое) значение, представляющее собой идентификатор, применяемый остальными функциями семафора. В случае ошибки возвращается -1.

semop

Функция semop применяется для изменения значения семафора.

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

Первый параметр sem_id — идентификатор семафора, возвращенный функцией semget. Второй параметр sem_ops — указатель на массив структур, у каждой из которых есть, по крайней мере, следующие элементы:

struct sembuf {

 short sem_num;

 short sem_op;

 short sem_flg;

}

Первый параметр sem_num — номер семафора, обычно 0, если вы не работаете с массивом семафоров. Элемент sem_op — значение, на которое должен изменяться семафор. (Вы можете увеличивать и уменьшать семафор на значения, не равные 1.) Как правило, применяются только два значения: -1 для операции P, заставляющей ждать, пока семафор не станет доступен, и +1 для операции V, оповещающей о том, что в данный момент семафор доступен.

Последний элемент sem_flg обычно задается равным SEM_UNDO. Это значение заставляет операционную систему отслеживать изменения значения семафора, сделанные текущим процессом, и, если процесс завершается, не освободив семафор, позволяет операционной системе автоматически освободить семафор, если он удерживался этим процессом. Хорошо взять за правило установку sem_flg, равным SEM_UNDO, если вам не требуется иного поведения. Если же вы все-таки решили, что вам нужно значение, отличное от SEM_UNDO, очень важно быть последовательным, иначе вы можете оказаться в замешательстве относительно попыток ядра системы "убрать" ваши семафоры, когда ваш процесс завершается.

Все действия, предусмотренные semop, собраны вместе, чтобы избежать состояния гонок, вызванного использованием множественных семафоров. Все подробности функционирования semop можно найти на страницах интерактивного справочного руководства.

semctl

Функция semctl позволяет напрямую управлять данными семафора.

int semctl (int sem_id, int sem_num, int command, ...);

Первый параметр sem_id — идентификатор семафора, полученный от функции semget. Параметр sem_num — номер семафора. Он применяется при работе с массивом семафоров. Обычно этот параметр равен 0, первый и единственный семафор. Параметр command — предпринимаемое действие, и четвертый параметр, если присутствует, — union (объединение) типа semun, которое в соответствии со стандартом X/Open должно содержать как минимум следующие элементы:

union semun {

 int val;

 struct semid_ds *buf;

 unsigned short *array;

}

В большинстве версий ОС Linux определение объединения semun включено в заголовочный файл (обычно sem.h), несмотря на то, что стандарт X/Open настаивает на том, что вы должны привести собственное объявление. Если вы поймете, что должны объявить его самостоятельно, проверьте, нет ли объявления этого объединения на страницах интерактивного справочного руководства, относящихся к функции semctl. Если вы найдете его, мы полагаем, что вы примените определение из вашего справочного руководства, даже если оно отличается от приведенного на страницах этой книги.

Существует множество разных значений параметра command, допустимых в функции semctl. Обычно применяются два из них, которые описаны далее. Более подробную информацию о функции semctl см. в интерактивном справочном руководстве.

Два часто используемых значения command таковы:

□ SETVAL — применяется для инициализации семафора с заданным значением. Это значение передается как элемент val объединения semun. Такое действие необходимо для того, чтобы увеличить значение семафора перед первым его применением;

□ IPC_RMID — применяется для удаления идентификатора семафора, когда он больше не нужен.

Функция semctl возвращает разные значения, зависящие от параметра command. Если значение команды — IPC_RMID, функция в случае успешного завершения вернет 0 и -1 в противном случае.

Применение семафоров

Как видно из содержания предыдущих разделов, операции с семафорами могут быть очень сложными. Это не самое печальное, потому что программирование многих процессов или потоков с критическими секциями — очень трудная задача сама по себе, и наличие сложного программного интерфейса лишь увеличивает интеллектуальную нагрузку.

К счастью, большинство задач, нуждающихся в семафорах, можно решить, применяя единственный бинарный семафор — простейший тип семафора. В следующем примере (упражнение 14.1) вы используете полный программный интерфейс для создания очень простого интерфейса типа Р и V для бинарного семафора. Затем вы примените этот простенький интерфейс для демонстрации того, как функционируют семафоры.

В экспериментах с семафорами будет использоваться единственная программа sem1.с, которую вы сможете запускать несколько раз. Необязательный параметр будет применяться для того, чтобы показать, отвечает ли программа за создание и уничтожение семафора.

Вывод двух разных символов будет обозначать вход в критическую секцию и выход из нее. Программа, запущенная с параметром, выводит X при входе в критическую секцию и выходе из нее. Другие экземпляры запущенной программы будут выводить символ О при входе в свои критические секции и выходе из них. Поскольку в любой заданный момент времени только один процесс способен войти в свою критическую секцию, все символы X и O должны появляться парами.

Упражнение 14.1. Семафоры

1. После системных директив #include вы включаете файл semun.h. Он определяет объединение типа semun в соответствии со стандартом X/Open, если оно уже не описано в системном файле sys/sem.h. Далее следуют прототипы функций и глобальная переменная, расположенные перед входом в функцию main. В ней создается семафор с помощью вызова semget, который возвращает ID семафора. Если программа вызывается первый раз (т.е. вызывается с параметром и argc > 1), выполняется вызов set_semvalue для инициализации семафора и переменной op_char присваивается значение O.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <sys/sem.h>

#include "semun.h"

static int set_semvalue(void);

static void del_semvalue(void);

static int semaphore_p(void);

static int semaphore_v(void);

static int sem_id;

int main(int argc, char *argv[]) {

 int i;

 int pause_time;

 char op_char = 'О';

 srand((unsigned int)getpid());

 sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

 if (argc >1) {

  if (!set_semvalue()) {

   fprintf(stderr, "Failed to initialize semaphore\n");

   exit(EXIT_FAILURE);

  }

  op_char = 'X';

  sleep(2);

 }

2. Далее следует цикл, в котором 10 раз выполняется вход в критическую секцию и выход из нее. Вы сначала выполняете вызов функции semaphore_p, которая заставляет семафор ждать, когда эта программа будет готова войти в критическую секцию.

 for (i = 0; i < 10; i++) {

  if (!semaphore_p()) exit(EXIT_FAILURE);

  printf("%c", op_char);

  fflush(stdout);

  pause_time = rand() % 3;

  sleep(pause_time);

  printf("%c", op_char);

  fflush(stdout);

3. После критической секции вы вызываете функцию semaphore_v, которая освобождает семафор перед повторным проходом цикла for после ожидания в течение случайного промежутка времени. После цикла выполняется вызов функции del_semvalue для очистки кода.

  if (!semaphore_v()) exit(EXIT_FAILURE);

  pause_time = rand() % 2;

  sleep(pause_time);

 }

 printf("\n%d - finished\n", getpid());

 if (argc > 1) {

  sleep(10);

  del_semvalue();

 }

 exit(EXIT_SUCCESS);

}

4. Функция set_semvalue инициализирует семафор с помощью команды SETVAL в вызове semctl. Это следует сделать перед использованием семафора.

static int set_semvalue(void) {

 union semun sem_union;

 sem_union.val = 1;

 if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0);

 return(1);

}

5. У функции del_semvalue почти та же форма за исключением того, что в вызове semctl применяется команда IPC_RMID для удаления ID семафора.

static void del_semvalue(void) {

 union semun sem_union;

 if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)

  fprintf(stderr, "Failed to delete semaphore\n");

}

6. Функция semaphore_p изменяет счетчик семафора на -1. Это операция ожидания или приостановки процесса.

static int semaphore_p(void) {

 struct sembuf sem_b;

 sem_b.sem_num = 0;

 sem_b.sem_op = -1; /* P() */

 sem_b.sem_flg = SEM_UNDO;

 if (semop(sem_id, &sem_b, 1) == -1) {

  fprintf(stderr, "semaphore_p failed\n");

  return(0);

 }

 return(1);

}

7. Функция semaphore_v аналогична за исключением задания элемента sem_op структуры sembuf, равного 1. Это операция "освобождения", в результате которой семафор снова становится доступен.

static int semaphore_v(void) {

 struct sembuf sem_b;

 sem_b.sem_num = 0;

 sem_b.sem_op = 1; /* V() */

 sem_b.sem_flg = SEM_UNDO;

 if (semop(sem_id, &sem_b, 1) == -1) {

  fprintf(stderr, "semaphore_v failed\n");

  return(0);

 }

 return(1);

}

Обратите внимание на то, что эта простая программа разрешает существование единственного двоичного семафора для каждой программы, хотя можно было бы увеличить количество, передав переменную семафора при необходимости. Обычно одного бинарного семафора достаточно.

Вы можете протестировать вашу программу, запустив ее несколько раз. В первый раз вы передадите параметр, чтобы сообщить программе о том, что она отвечает за создание и удаление семафора. У других экземпляров выполняющейся программы не будет параметра.

Далее приведен примерный вывод для двух запущенных экземпляров программы:

$ cc sem1.с -о sem1

$ ./sem1 1 &

[1] 1082

$ ./sem1

OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX

1083 - finished

1082 - finished

Напоминаем, что символ О представляет первый запущенный экземпляр программы, а символ X — второй экземпляр выполняющейся программы. Поскольку каждый экземпляр программы выводит символ при входе в критическую секцию и при выходе из нее, каждый символ должен появляться только попарно. Как видите, символы О и Х на самом деле образуют пары, указывая на корректную обработку критических секций. Если программа не работает на вашей системе, можно применить команду stty -tostop перед запуском программы, чтобы гарантировать, что фоновая программа, генерирующая вывод на tty, не вызывает возбуждение сигнала.

Как это работает

Программа начинается с получения обозначения семафора на основе ключа (произвольного), который вы выбрали, применив функцию semget. Флаг IPC_CREAT приводит к созданию семафора, если он нужен.

Если у программы есть параметр, она отвечает за инициализацию семафора, которая выполняется функцией set_semvalue, упрощенным вариантом функции общего назначения semctl. Она также использует наличие параметра для определения символа вывода. Функция sleep просто предоставляет некоторое время для запуска других экземпляров программы до того, как данная программа выполнит слишком много проходов своего цикла. Для включения в программу нескольких псевдослучайных промежутков времени вы используете функции srand и rand.

Далее программа выполняет 10 раз операторы тела цикла с псевдослучайными периодами ожидания в своей критической и некритической секциях. Критическая секция охраняется вызовами ваших функций semaphore_p и semaphore_v, упрощенных интерфейсов функции более общего вида semop.

Перед удалением семафора программа, запущенная с параметром, ждет, пока завершится выполнение других экземпляров программы. Если семафор не удален, он будет продолжать существовать в системе, даже если нет программ, его использующих. В реальных программах очень важно убедиться в том, что вы случайно не оставили семафор после завершения выполнения. Он может вызвать проблемы при следующем запуске программы, кроме того, семафоры — разновидность ограниченных ресурсов, которые вы должны беречь.

Совместно используемая память

Совместно используемая или разделяемая память — вторая разновидность средств IPC. Она позволяет двум несвязанным процессам обращаться к одной и той же логической памяти. Хотя стандарт X/Open не требует этого, надо полагать, что большинство реализаций разделяемой памяти размещают память, совместно используемую разными процессами, так, что она ссылается на одну и ту же физическую память.

Совместно используемая память — это специальный диапазон адресов, создаваемых средствами IPC для одного процесса и включаемых в адресное пространство этого процесса. Другой процесс может затем "присоединить" тот же самый сегмент совместно используемой памяти к своему адресному пространству. Все процессы могут получать доступ к участкам памяти так, как будто эта память была выделена функцией malloc. Если один процесс записывает в совместно используемую память, изменения немедленно становятся видимыми любому другому процессу, имеющему доступ к этой совместно используемой памяти.

Совместно используемая память обеспечивает эффективный способ разделения и передачи данных между разными процессами. Сама по себе совместная используемая память не предоставляет никаких средств синхронизации, поэтому вы, как правило, вынуждены применять некоторые другие механизмы для синхронизации доступа к совместно используемой памяти. Обычно совместно используемая память применяется для обеспечения эффективного доступа к обширным областям памяти, а для синхронизации доступа к ней передаются небольшие сообщения.

Не существует автоматических средств для того, чтобы помешать второму процессу начать считывание совместно используемой памяти до того, как первый процесс закончит запись в нее. За синхронизацию доступа отвечает программист. На рис. 14.2 показан принцип работы совместно используемой памяти.

Рис. 14.2

Стрелки показывают отображение логического адресного пространства каждого процесса на доступную физическую память. На практике ситуация сложнее, потому что доступная память на самом деле представляет собой смесь физической памяти и страниц памяти, которые были выгружены на диск.

Функции для работы с совместно используемой памятью напоминают функции семафоров:

#include <sys/shm.h>

void *shmat(int shm_id, const void *shm_addr, int shmflg);

int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

int shmdt(const void *shm_addr);

int shmget(key_t key, size_t size, int shmflg);

Как и в случае семафоров, заголовочные файлы sys/types.h и sys/ipc.h автоматически включаются в программу файлом shm.h.

shmget

Создается совместно используемая память с помощью функции shmget:

int shmget(key_t key, size_t size, int shmflg);

Как и для семафоров, программа предоставляет key, фактически именующий сегмент совместно используемой памяти, а функция shmget возвращает идентификатор совместно используемой памяти, который применяется всеми последующими функциями для работы с этой областью памяти. Есть особое значение ключа IPC_PRIVATE, создающее для процесса частную, скрытую от других совместно используемую память. Обычно вы не будете пользоваться этим значением, да и кроме всего прочего в некоторых системах Linux можете обнаружить, что такая частная разделяемая память на самом деле далеко не частная.

Второй параметр size задает требуемый объем памяти в байтах.

Третий параметр shmflg содержит девять флагов прав доступа, которые используются так же, как флаги режима создающихся файлов. Для создания нового сегмента совместно используемой памяти специальный бит, описываемый IPC_CREAT, должен с помощью поразрядной операции OR быть объединен с правами доступа. Не считается ошибкой задание флага IPC_CREAT и передача ключа существующего сегмента совместно используемой памяти. Флаг IPC_CREAT, если в нем нет нужды, беззвучно игнорируется.

Флаги прав доступа к совместно используемой памяти очень полезны, поскольку позволяют процессу создать совместно используемую память, в которую могут писать процессы, принадлежащие создателю этой разделяемой памяти, а процессы, созданные другими пользователями, могут только читать этот сегмент памяти. Вы можете использовать этот механизм для обеспечения эффективного доступа к данным только для чтения, поместив их в совместно используемую память без какого- либо риска их повреждения другими пользователями.

Если совместно используемая память создана успешно, shmget вернет неотрицательное целое, идентификатор совместно используемой памяти. В случае аварийного завершения функция вернет -1.

shmat

Когда вы впервые создаете сегмент совместно используемой памяти, он недоступен ни одному процессу. Для того чтобы обеспечить доступ к совместно используемой памяти, нужно присоединить ее к адресному пространству процесса. Делается это с помощью функции shmat:

void *shmat(int shm_id, const void *shm_addr, int shmflg);

Первый параметр shm_id — идентификатор совместно используемой области памяти, возвращаемый функцией shmget.

Второй параметр shm_addr — адрес, по которому совместно используемая память присоединяется к текущему процессу. Почти всегда его следует задавать пустым указателем, что позволяет системе выбрать адрес для доступа к совместно используемой памяти.

Третий параметр shmflg — набор поразрядных флагов. Два возможных значения: SHM_RND, в сочетании с shm_addr управляющее адресом, по которому присоединяется к процессу совместно используемая память, и SHM_RDONLY, которое делает присоединенную память доступной только для чтения. Очень редко возникает необходимость управлять адресом присоединения совместно используемой памяти. Как правило, следует позволить системе выбрать для вас адрес, поскольку в противном случае приложение станет в значительной степени аппаратно-зависимым.

Если вызов shmat завершился успешно, он вернет указатель на первый байт совместно используемой памяти. В случае аварийного завершения возвращается -1.

Наличие доступа для чтения совместно используемой памяти и записи в нее зависит от владельца (создателя сегмента совместно используемой памяти), прав доступа и владельца текущего процесса. Права доступа к совместно используемой памяти подобны правам доступа к файлам.

Исключение из этого правила возникает, если выражение shmflg & SHM_RDONLY равно true. В этом случае в совместно используемую память нельзя писать, даже если права доступа предоставляют такую возможность.

shmdt

Функция shmdt отсоединяет совместно используемую память от текущего процесса. Она принимает указатель на адрес, возвращенный функцией shmat. В случае успеха функция вернет 0, в случае ошибки - -1. Имейте в виду, что отсоединение совместно используемой памяти не уничтожает ее, а только делает эту память недоступной для текущего процесса.

shmctl

Функции управления совместно используемой памятью (к счастью) гораздо проще аналогичных, но более сложных функций для семафоров:

int shmctl(int shm_id, int command, struct shmid_ds *buf);

У структуры типа shmid_ds есть, как минимум, следующие элементы:

struct shmid_ds {

 uid_t shm_perm.uid;

 uid_t shm_perm.gid;

 mode_t shm_perm.mode;

}

Первый параметр shm_id — идентификатор, возвращаемый функцией shmget.

Второй параметр command содержит предпринимаемое действие. Он может принимать три значения, перечисленные в табл. 14.2.

Таблица 14.2

Значение Описание
IPC_STAT Задаёт данные в структуре shmid_ds, отображающие значения, связанные с совместно используемой памятью
IPC_SET Устанавливает значения, связанные с совместно используемой памятью в соответствии с данными из структуры типа shmid_ds, если у процесса есть право на это действие
IPC_RMID Удаляет сегмент совместно используемой памяти

Третий параметр buf — указатель на структуру, содержащую режимы и права доступа для совместно используемой памяти.

В случае успеха возвращает 0, в случае ошибки — -1. В стандарте X/Open не описано, что произойдет, если вы попытаетесь удалить присоединенный к процессу сегмент совместно используемой памяти. Обычно присоединенный, но удаленный сегмент совместно используемой памяти продолжает функционировать до тех пор, пока не будет отсоединен от последнего процесса. Но поскольку это поведение не задано в стандарте, на него лучше не рассчитывать.

Выполните упражнение 14.2.

Упражнение 14.2. Совместно используемая память

После знакомства с функциями совместно используемой памяти можно написать программу для их использования. В данном упражнении вы напишите пару программ: shm1.c и shm2.c. Первая (потребитель) создаст сегмент разделяемой памяти и затем отобразит любые данные, записанные в него. Вторая (поставщик) присоединит существующий сегмент совместно используемой памяти и позволит вам ввести данные в этот сегмент.

1. Сначала создайте общий заголовочный файл для описания совместно используемой памяти, которую вы хотите предоставить. Назовите его shm_com.h.

#define TEXT_SZ 2048

struct shared_use_st {

 int written_by_you;

 char some_text[TEXT_SZ];

};

В файле определена структура, которая будет применяться в обеих программах: потребителе и поставщике. Вы используете флаг written_by_you типа int для того, чтобы сообщить потребителю о том, что данные записаны в оставшуюся часть структуры, и произвольно решаете, что необходимо передать до 2 Кбайт текста.

2. Первая программа shm1.c — потребитель. После заголовочных файлов создается сегмент совместно используемой памяти (размер равен вашей структуре, описывающей совместно используемую память) с помощью вызова shmget с заданным битом IPC_CREAT.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <sys/shm.h>

#include "shm_com.h"

int main() {

 int running = 1;

 void *shared_memory = (void *)0;

 struct shared_use_st *shared_stuff;

 int shmid;

 srand((unsigned int)getpid());

 shmid = shmget((key_t)1234, sizeof(struct shared_use_st),

  0666 | IPC_CREAT);

 if (shmid == -1) {

  fprintf(stderr, "shmget failed\n");

  exit(EXIT_FAILURE);

 }

3. Теперь вы делаете совместно используемую память доступной программе.

 shared_memory = shmat(shmid, (void *)0, 0);

 if (shared memory == (void *)-1) {

  fprintf(stderr, "shmat failed\n");

  exit(EXIT_FAILURE);

 }

 printf("Memory attached at %X\n", (int)shared_memory);

4. В следующем фрагменте программы сегмент shared_memory присваивается переменной shared_stuff, из которой затем выводится любой текст, содержащийся в some_text. Цикл продолжает выполняться до тех пор, пока не найдена строка end в элементе some_text. Вызов функции sleep заставляет программу-потребителя оставаться в своей критической секции, что вынуждает поставщика ждать.

 shared_stuff = (struct shared_use_st *)shared_memory;

 shared_stuff->written_by_you = 0;

 while (running) {

  if (shared_stuff->written_by_you) {

   printf("You wrote: %s", shared_stuff->some_text);

   sleep(rand() % 4);

   /* Заставляет другой процесс ждать нас! */

   shared_stuff->written_by_you = 0;

   if (strncmp(shared_stuff->some_text, "end", 3) == 0) {

    running = 0;

   }

  }

 }

5. В заключение совместно используемая память отсоединяется и удаляется.

 if (shmdt(shared_memory) == -1) {

  fprintf(stderr, "shmdt failed\n");

  exit(EXIT_FAILURE);

 }

 if (shmctl(shmid, IPC_RMID, 0) == -1) {

  fprintf(stderr, "shmctl(IPC_RMID) failed\n");

  exit(EXIT_FAILURE);

 }

 exit(EXIT_SUCCESS);

}

6. Вторая программа shm2.c — поставщик; она позволяет вводить данные для потребителей. Программа очень похожа на shm1.c и выглядит следующим образом.

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <sys/shm.h>

#include "shm_com.h"

int main() {

 int running = 1;

 void *shared_memory = (void *)0;

 struct shared_use_st *shared_stuff;

 char buffer[BUFSIZ];

 int shmid;

 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);

 if (shmid == -1) {

  fprintf(stderr, "shmget failed\n");

  exit(EXIT_FAILURE);

 }

 shared_memory = shmat(shmid, (void *)0, 0);

 if (shared_memory == (void *)-1) {

  fprintf(stderr, "shmat failed\n");

  exit(EXIT_FAILURE);

 }

 printf("Memory attached at %X\n", (int)shared_memory);

 shared_stuff = (struct shared_use_st *)shared_memory;

 while (running) {

  while (shared_stuff->written_by_you == 1) {

   sleep(1);

   printf("waiting for client...\n");

  }

  printf("Enter same text: ");

  fgets(buffer, BUFSIZ, stdin);

  strncpy(shared_stuff->some_text, buffer, TEXT_SZ);

  shared_stuff->written_by_you = 1;

  if (strncmp(buffer, "end", 3) == 0) {

   running = 0;

  }

 }

 if (shmdt(shared_memory) == -1) {

  fprintf(stderr, "shmdt failed\n");

  exit(EXIT_FAILURE);

 }

 exit(EXIT_SUCCESS);

}

Когда вы выполните эти программы, то получите образец вывода, подобный следующему:

$ ./shm1 &

[1] 294

Memory attached at 40017000

$ ./shm2

Memory attached at 40017000

Enter some text: hello

You wrote: hello

waiting for client...

waiting for client...

Enter some text: Linux!

You wrote: Linux!

waiting for client...

waiting for client...

waiting for client...

Enter some text: end

You wrote: end

$

Как это работает

Первая программа shm1 создает сегмент совместно используемой памяти и затем присоединяет его к своему адресному пространству. Вы накладываете структуру shared_use_st на начальную область совместно используемой памяти. У нее есть флаг written_by_you, который устанавливается, когда данные доступны. Если флаг установлен, программа считывает текст, выводит его и сбрасывает флаг, чтобы показать, что данные прочитаны. Для корректного выхода из цикла примените специальную строку end. Далее программа отсоединяет сегмент совместно используемой памяти и удаляет его.

Вторая программа shm2 получает и присоединяет тот же самый сегмент совместно используемой памяти, поскольку она применяет тот же ключ 1234. Затем она просит пользователя ввести текст. Если флаг written_by_you установлен, shm2 знает, что клиентский процесс еще не считал предыдущую порцию данных и ждет завершения чтения. Когда другой процесс очищает флаг, shm2 записывает новые данные и устанавливает флаг. Она также пользуется магической строкой end для завершения записи и отсоединения сегмента совместно используемой памяти.

Обратите внимание на то, что вы вынуждены с помощью флага written_by_you предоставить собственный очень грубый механизм синхронизации, который включает очень неэффективное активное ожидание (с непрерывным циклом). Такой подход сохраняет простоту примера, но в реальных программах вам следует применить семафор либо передать сообщение с помощью неименованного канала или сообщений IPC (которые будут обсуждаться в следующем разделе), либо сгенерировать сигнал (как показано в главе 11), чтобы обеспечить более эффективный механизм синхронизации между читающей и пишущей частями приложения.

Очереди сообщений

Теперь рассмотрим третье и последнее средство System V IPC: очереди сообщений. Во многом очереди сообщений похожи на именованные каналы, но без сложностей, сопровождающих открытие и закрытие канала. Однако применение очереди сообщений не избавляет вас от проблем, возникающих при использовании именованных каналов, например блокировки заполненных каналов.

Очереди сообщений предоставляют очень легкий и эффективный способ передачи данных между двумя несвязанными процессами. У них есть преимущество по сравнению с именованными каналами, заключающееся в том, что очередь сообщений существует независимо как от отправляющего, так и от принимающего процессов, что устраняет некоторые трудности, возникающие при синхронизации открытия и закрытия именованных каналов.

Очереди сообщений обеспечивают отправку блока данных из одного процесса в другой. Кроме того, каждый блок данных наделяется типом, и принимающий процесс может получать независимо блоки данных, имеющие разные типы. Хорошо и то, что, отправляя сообщения, вы можете почти полностью избежать проблем синхронизации и блокировки, связанных с именованными каналами. Еще лучше то, что вы можете проявить предусмотрительность в отношении неотложных в том или ином смысле сообщений. К недостаткам следует отнести то, что, как и в случае каналов, в системе существует ограничение максимального объема блока данных и максимального объема всех блоков данных во всех очередях.

Наложив эти ограничения, стандарт X/Open не позаботился о способе выяснения их числовых значений за исключением того, что превышение ограничений — достаточное основание для аварийного завершения функций обработки очереди сообщений. В ОС Linux есть два определения: MSGMAX и MSGMNB, которые задают максимальный объем в байтах отдельного сообщения и максимальный объем очереди соответственно. В других системах эти макросы могут отличаться или просто отсутствовать.

Далее приведены объявления функций для работы с очередями сообщений:

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

int msgget(key_t key, int msgflg);

int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

Как и в случае семафоров или совместно используемой памяти, заголовочные файлы sys/types.h и sys/ipc.h обычно автоматически включаются заголовочным файлом msg.h.

msgget

Очередь сообщений создается и предоставляет к себе доступ с помощью функции msgget:

int msgget(key_t key, int msgflg);

Программа должна предоставить значение параметра key, которое, как и в других средствах IPC, задает имя конкретной очереди сообщений. С помощью специального значения IPC_PRIVATE создается скрытая или частная очередь, которая теоретически доступна только текущему процессу. Как и в случае семафоров и совместно используемой памяти, в некоторых системах Linux такая очередь может не быть частной. Поскольку от скрытой или частной очереди очень мало пользы, это не слишком важная проблема. Как и раньше, второй параметр msgflg состоит из девяти флагов прав доступа. Для создания новой очереди сообщений специальный бит со значением IPC_CREAT должен быть объединен с правами доступа поразрядной операцией OR. Не считается ошибкой установка флага IPC_CREAT и задание ключа уже существующей очереди сообщений. Если очередь уже есть, флаг IPC_CREAT безмолвно игнорируется.

Функция msgget вернет положительное число, идентификатор очереди; в случае успешного завершения и -1 в случае сбоя.

msgsnd

Функция msgsnd позволяет добавить сообщение в очередь сообщений:

int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

Структура сообщения ограничена двумя способами. Во-первых, она должна быть меньше системного ограничения, и во-вторых, она должна начинаться с элемента типа long int, который будет использован как тип сообщения в получающей функции. Если вы применяете сообщения, лучше всего определить структуру сообщения следующим образом.

struct my_message {

 long int message_type;

 /* Данные, которые вы собираетесь передавать */

}

Поскольку элемент message_type используется при получении сообщения, вы не можете его просто игнорировать. Вы должны включить его в вашу структуру данных, и будет разумно инициализировать его с помощью известного значения.

Первый параметр msqid — идентификатор очереди сообщений, возвращаемый функцией msgget.

Второй параметр msg_ptr — указатель на отправляемое сообщение, которое должно начинаться с элемента типа long int, как описывалось ранее.

Третий параметр msg_sz — объем сообщения, на которое указывает msg_ptr. Этот объем не должен включать элемент типа long int, содержащий тип сообщения.

Четвертый параметр msgflg управляет действиями, предпринимаемыми при заполнении текущей очереди сообщений или достижении общесистемного ограничения для очередей сообщений. Если в параметре msgflg установлен флаг IPC_NOWAIT, функция вернет управление немедленно без отправки сообщения и возвращаемое значение будет равно -1. Если в параметре msgflg флаг IPC_NOWAIT сброшен, процесс отправки будет приостановлен в ожидании освобождения доступного объема в очереди.

В случае успеха функция вернет 0, а в случае аварийного завершения — -1. Если вызов был успешен, копия данных сообщения принимается и помещается в очередь сообщений.

msgrcv

Функция msgrcv извлекает сообщения из очереди сообщений:

int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

Первый параметр msqid — идентификатор очереди сообщений, возвращенный функцией msgget.

Второй параметр msg_ptr — указатель на получаемое сообщение, которое должно начинаться с элемента типа long int, как описывалось ранее в функции msgsnd.

Третий параметр msg_sz — размер сообщения, на которое указывает msg_ptr, без элемента типа long int, содержащего тип сообщения.

Четвертый параметр msgtype типа long int позволяет реализовать простую форму приоритетного получения. Если значение msgtype равно 0, извлекается первое доступное сообщение в очереди. Если значение параметра больше нуля, извлекается первое сообщение с таким же типом сообщения. Если оно меньше нуля, извлекается первое сообщение с таким же типом сообщения или со значением, по абсолютной величине меньшим, чем msgtype.

На практике все гораздо проще. Если вы просто хотите получать сообщения в порядке их отправления, задайте msgtype, равным 0. Если нужно извлекать сообщения только с определенным типом, задайте msgtype, равным этому значению. Если вам необходимо получать сообщения с типом не превышающим n, задайте msgtype, равным -n.

Четвертый параметр msgflg управляет действиями в случае отсутствия сообщения подходящего типа, которое ожидает извлечения. Если в параметре msgflg установлен флаг IPC_NOWAIT, вызов вернет управление программе немедленно с возвращаемым значением -1. Если флаг IPC_NOWAIT в msgflg сброшен, процесс будет приостановлен в ожидании прибытия сообщения подходящего типа.

В случае успешного завершения функция msgrcv вернет количество байтов, помещенных в буфер приема, сообщение копируется в выделяемый пользователем буфер, на который указывает msg_ptr, и данные удаляются из очереди сообщений. В случае ошибки функция вернет -1.

msgctl

Последняя функция обработки очереди сообщений msgctl очень похожа на функцию управления для совместно используемой памяти:

int msgctl(int msqid; int command, struct msqid_ds *buf);

Структура msqid_ds содержит, как минимум, следующие элементы:

struct msqid_ds {

 uid_t msg_perm.uid;

 uid_t msg_perm.gid;

 mode_t msg_perm.mode;

}

Первый параметр msqid — идентификатор, возвращаемый функцией msgget.

Второй параметр command задает предпринимаемое действие. Он может принимать три значения, перечисленные в табл. 14.3.

Таблица 14.3

Значение Описание
IPC_STAT Задает данные в структуре msqid_ds, отображающие значения, связанные с очередью сообщений
IPC_SET Если у процесса есть на это право, это действие устанавливает значения, связанные с очередью сообщений, в соответствии с данными структуры msqid_ds
IPC_RMID Удаляет очередь сообщений

В случае успешного завершения возвращает 0, в случае аварийного — -1. Если очередь сообщений удаляется, когда процесс ожидает в функции msgsnd или msgrcv, функция отправки или получения сообщения завершается аварийно.

Выполните упражнение 14.3.

Упражнение 14.3. Очереди сообщений

Теперь, когда вы познакомились с объявлениями, относящимися к очередям сообщений, можно посмотреть, как они действуют на практике. Как и раньше, вы напишите две программы: msg1.c для получения и msg2.c для отправки сообщений. Вы разрешите обеим программам создавать очередь сообщений, но используете для удаления очереди программу-приемник после того, как она получит последнее сообщение.

1. Далее приведена программа-приемник msg1 .с:

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <sys/msg.h>

struct my_msg_st {

 long int my_msg_type;

 char some_text[BUFSIZ];

};

int main() {

 int running = 1;

 int msgid;

 struct my_msg_st some_data;

 long int msg_to_receive = 0;

2. Прежде всего, задайте очередь сообщений:

 msgid = msgget((key_t)1234, 0666 | IPC_CREAT);

 if (msgid == -1) {

  fprintf(stderr, "msgget failed with error: %d\n", errno);

  exit(EXIT_FAILURE);

 }

3. Далее сообщения извлекаются из очереди до тех пор, пока не будет обнаружено сообщение end. В конце очередь сообщений удаляется.

 while (running) {

  if (msgrcv(msgid, (void *)&some_data, BUFSIZ, msg_to_receive, 0) == -1) {

   fprintf(stderr, "msgrcv failed with error: %d\n", errno);

   exit(EXIT_FAILURE);

  }

  printf("You wrote: %s", some_data.some_text);

  if (strncmp(some_data.some_text, "end", 3) == 0) {

   running = 0;

  }

 }

 if (msgctl(msgid, IPC_RMID, 0) == -1) {

  fprintf(stderr, "msgctl(IPC_RMID) failed\n");

  exit(EXIT_FAILURE);

 }

 exit(EXIT_SUCCESS);

}

4. Программа-отправитель msg2.c очень похожа на программу msg1.с. В функции main удалите объявление msg_to_receive и замените его переменной buffer[BUFSIZ]. Уберите из программы удаление очереди и внесите следующие изменения в цикл с управляющей переменной running. Теперь у вас появился вызов функции msgsnd для отправки введенного текста в очередь сообщений. Далее приведена программа msg2.c с отличиями от программы msg1.с, выделенными цветом.

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <sys/msg.h>

#define MAX_TEXT 512

struct my_msg_st {

 long int my_msg_type;

 char some_text[MAX_TEXT];

};

int main() {

 int running = 1;

 struct my_msg_st some_data;

 int msgid;

 char buffer = [BUFSIZ];

 msgid = msgget((key_t)1234, 0666 | IPC_CREAT);

 if (msgid == -1) {

  fprintf(stderr, "msgget failed with error: %d\n", errno);

  exit(EXIT_FAILURE);

 }

 while (running) {

  printf("Enter some text: ");

  fgets(buffer, BUFSIZ, stdin);

  some_data.my_msg_type = 1;

  strcpy(some_data.some_text, buffer);

  if (msgsnd(msgid, (void*)&some_data, MAX_TEXT, 0)) == -1) {

   fpintf(stderr, "msgsnd failed\n");

   exit(EXIT_FAILURE);

  }

  if (strncmp(buffer, "end", 3) == 0) {

   running = 0;

  }

 }

 exit(EXIT_SUCCESS);

}

В отличие от примера с каналами, процессам нет нужды предоставлять метод их собственной синхронизации. Это существенное преимущество сообщений по сравнению с каналами.

Если в очереди сообщений есть место, отправитель может создать очередь, поместить в нее какие-либо данные и завершить выполнение еще до того, как начнет выполняться приемник. Первой следует запускать программу-отправителя msg2. Далее приведен пример вывода:

$ ./msg2

Enter some text: hello

Enter some text: How are you today?

Enter some text: end

$ ./msg1

You wrote: hello

You wrote: How are you today?

You wrote: end

Как это работает

Программа-отправитель создает очередь сообщений с помощью функции msgget; далее она добавляет сообщения в очередь, применяя функцию msgsnd. Программа-приемник получает идентификатор очереди сообщений с помощью функции msgget и получает сообщения до тех пор, пока не будет найден специальный текст end. Затем программа приводит все в порядок, удаляя очередь сообщений с помощью функции msgctl.

Приложение для работы с базой данных компакт-дисков

Сейчас подходящий момент для модификации вашего приложения, управляющего базой данных компакт-дисков, с помощью средств IPC, с которыми вы познакомились в этой главе.

Вы могли бы применить множество разных комбинаций трех разновидностей средств IPC, но поскольку информация, которую следует передавать, очень мала по объему, есть смысл реализовать передачу запросов и ответов непосредственно с помощью очередей сообщений.

Если объемы данных, которые вы должны передавать, были бы велики, можно было бы рассмотреть передачу реальных данных в совместно используемой памяти, одновременно применяя семафоры или сообщения для отправки маркера или "опознавательного знака", информирующего другой процесс о наличии данных в совместно используемой памяти.

Интерфейс очереди сообщений устраняет проблему, которая у вас была в главе 11, когда вы нуждались в открытом канале у обоих процессов в момент передачи данных. Применение очередей сообщений позволяет одному процессу поместить сообщения в очередь, даже если этот процесс в данный момент — единственный пользователь очереди.

Вам нужно ответить лишь на один важный вопрос: как возвращать ответы клиентам? Простым решением было бы наличие одной очереди для сервера и по одной очереди для каждого клиента. Если одновременно существует много клиентов, такой подход может вызвать проблемы, т.к. потребуется большое количество очередей. Используя в сообщении поле идентификатора сообщения, вы сможете разрешить всем клиентам пользоваться одной очередью и адресовать ответные сообщения конкретным клиентским процессам с помощью включенного в сообщение идентификатора клиентского процесса. Далее каждый клиент может извлекать сообщения, адресованные только ему, оставляя сообщения для других клиентов в очереди.

Для преобразования приложения, работающего с базой данных компакт-дисков, с помощью средств IPC вам придется заменить только файл pipe_imp.c из сопроводительного программного кода к главе 13. Далее мы рассмотрим важные разделы замещающего файла ipc_imp.c.

Пересмотр функций сервера

Сначала нужно обновить серверные функции.

1. Прежде всего, включите необходимые заголовочные файлы, объявите несколько ключей очередей сообщений и структуру для хранения данных сообщения:

#include "cd_data.h"

#include "cliserv.h"

#include <sys/msg.h>

#define SERVER_MQUEUE 1234

#define CLIENT_MQUEUE 4321

struct msg_passed {

 long int msg_key; /* Используется для клиентского pid */

 message_db_t real message;

};

2. Две глобальные переменные хранят идентификаторы двух очередей, возвращаемые функцией msgget:

static int serv_qid = -1;

static int cli_qid = -1;

3. Сделайте сервер ответственным за создание обеих очередей сообщений:

int server starting() {

#if DEBUG_TRACE

 printf("%d :- server_starting()\n", getpid());

#endif

 serv_qid = msgget((key_t)SERVER_MQUEUE, 0666 | IPC_CREAT);

 if (serv_qid == -1) return(0);

 cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666 | IPC_CREAT);

 if (cli_qid == -1) return(0);

 return(1);

}

4. За удаление очереди, если она существует, также отвечает сервер. Когда сервер заканчивает работу, вы задаете недопустимые значения вашим глобальным переменным. Это позволит выловить любые ошибки при попытке сервера отправить сообщения после вызова функции server_ending:

void server_ending() {

#if DEBUG_TRACE

 printf("%d :- server_ending()\n", getpid());

#endif

 (void)msgctl(serv_qid, IPC_RMID, 0);

 (void)msgctl(cli_qid, IPC_RMID, 0);

 servqid = -1;

 cliqid = -1;

}

5. Серверная функция read читает из очереди сообщение любого типа (т.е. от любого клиента) и возвращает часть сообщения с данными (пропуская тип сообщения):

int read_request_from_client(message_db_t *rec_ptr) {

 struct msg_passed my_msg;

#if DEBUG_TRACE

 printf("%d :- read_request_from_client()\n", getpid());

#endif

 if (msgrcv(serv_qid, (void *)&my_msg, sizeof(*rec_ptr), 0, 0) == -1) {

  return(0);

 }

 *rec_ptr = my_msg.real_message;

 return(1);

}

6. При отправке сообщения для его адресации используется ID клиентского процесса, хранящийся в запросе:

int send_resp_to_client(const message_db_t mess_to_send) {

 struct msg_passed my_msg;

#if DEBUG_TRACE

 printf("%d :- send_resp_to_client()\n", getpid());

#endif

 my_msg.real_message = mess_to_send;

 my_msg.msg_key = mess_to_send.client_pid;

 if (msgsnd(cli_qid, (void *)&my_msg, sizeof(mess_to_send), 0) == -1) {

  return(0);

 }

 return(1);

}

Пересмотр функций клиента

Теперь нужно внести изменения в клиентские функции.

1. Когда клиент стартует, ему нужно найти идентификаторы серверной и клиентской очередей. Клиент не создает очереди. Если сервер не работает, эта функция завершится аварийно, поскольку не существует очередей сообщений.

int client starting() {

#if DEBUG_TRACE

 printf("%d :- client_starting\n", getpid());

#endif

 serv_qid = msgget((key_t)SERVER_MQUEUE, 0666);

 if (serv_qid == -1) return(0);

 cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666);

 if (cli_qid == -1) return(0);

 return(1);

}

2. Как и в случае сервера, когда клиент завершает работу, вы задаете некорректные значения глобальных переменных. Это позволит выявить ошибки при попытке клиента отправлять сообщения после вызова функции client_ending.

void client_ending() {

#if DEBUG_TRACE

 printf("%d :- client_ending()\n", getpid());

#endif

 serv_qid = -1;

 cli_qid = -1;

}

3. Для отправки сообщения серверу сохраните данные в своей структуре. Учтите, что вы должны задать ключ сообщения. Поскольку 0 — недопустимое значение для ключа, незаданный ключ означает, что он принимает (очевидно) случайное значение, поэтому иногда эта функция может возвращать ошибку, если значение оказывается нулевым.

int send_mess_to_server(message_db_t mess_to_send) {

 struct msg_passed my_msg;

#if DEBUG_TRACE

 printf("%d send_mess_to_server()\n", getpid());

#endif

 my_msg.real_message = mess_to_send;

 my_msg.msg_key = mess_to_send.client_pid;

 if (msgsnd(serv_qid, (void *)&my_msg, sizeof(mess_to_send) , 0) == -1) {

  perror("Message send failed");

  return(0);

 }

 return(1);

}

4. При получении сообщения от сервера клиент использует ID процесса для получения только сообщений, адресованных ему, пропуская сообщения, предназначенные другим клиентам.

int read_resp_from_server(message_db_t *rec_ptr) {

 struct msg_passed mymsg;

#if DEBUG_TRACE

 printf("%d :- read_resp_from_server()\n", getpid());

#endif

 if (msgrcv(cli_qid, (void *)&my_msg, sizeof(*rec_ptr), getpid(), 0) == -1) {

  return(0);

 }

 *rec_ptr = my_msg.real_message;

 return(1);

}

5. Для сохранения совместимости с файлом pipe_imp.c необходимо объявить четыре дополнительные функции. Но в вашей программе они будут пустыми. Операции, которые они реализовывали в случае применения каналов, больше не нужны.

int start_resp_to_client(const message_db_t mess_to_send) {

 return(1);

}

void end_resp_to_client(void) {}

int start_resp_from_server(void) {

 return(1);

}

void end_resp_from_server(void) {}

Теперь вы можете просто запустить сервер, выполняющий в фоновом режиме реальное сохранение и извлечение данных, и затем выполнить клиентское приложение для подключения к серверу с помощью сообщений.

Все, что вы должны сделать, — это заменить интерфейсные функции из главы 11 другой реализацией, применяющей очереди сообщений. Преобразование приложения для использования очередей сообщений показывает мощь этого средства IPC, т.к. вам требуется меньше функций, чем в случае применения каналов, и даже эти необходимые функции гораздо проще, чем в предыдущей версии приложения.

Команды состояния IPC

Несмотря на то, что для соответствия требованиям X/Open этого не требуется, большинство систем Linux предоставляет набор команд, обеспечивающих доступ к данным IPC в режиме командной строки и удаление потерянных средств IPC. Существуют команды ipcs и ipcrm, очень полезные при разработке программ.

Один из досадных недостатков средств IPC состоит в том, что плохо написанная программа или программа, по какой-либо причине завершившаяся аварийно, может оставить свои ресурсы IPC (например, данные в очереди сообщений) еще долго блуждающими в системе без определенной цели после завершения программы. Такое поведение может привести к аварийному завершению нового запуска программы, поскольку она рассчитывает начать выполнение в очищенной системе, а на самом деле находит эти блуждающие ресурсы. Команды состояния (ipcs) и удаления (ipcrm) позволяют проверить систему и очистить ее от ненужных средств IPC.

Отображение состояния семафора

Для проверки состояния семафоров в системе примените команду ipcs -s. Если какие-то семафоры присутствуют, вывод команды будет выглядеть следующим образом:

$ ipcs -s

------ Semaphore Arrays ------

key        semid owner perms nsems

0x4d00df1a 768   rick  666   1

Для удаления семафоров, случайно оставленных программами, вы можете использовать команду ipcrm. Для удаления только что отображенного семафора примените (в Linux) следующую команду:

$ ipcrm -s 768

В некоторых более старых системах Linux используется несколько иной синтаксис команды:

$ ipcrm sem 768

Но этот устаревший стиль редко встречается в наше время. Формат, подходящий для вашей конкретной системы, ищите на страницах интерактивного справочного руководства.

Отображение состояния совместно используемой памяти

Многие системы предоставляют программы режима командной строки для доступа не только к сведениям о семафорах, но и к подробным данным совместно используемой памяти. К ним относятся команды ipcs -m и ipcrm -m <id> (или ipcrm shm <id>).

Далее приведен пример вывода команды ipcs -m:

$ ipcs -m

------ Shared Memory Segments ------

key        shmid owner perms bytes nattch status

0x00000000 384   rick  666   4096  2      dest

Здесь показан единственный сегмент совместно используемой памяти объемом 4 Кбайт, присоединенный к двум процессам.

Команда ipcrm -m <id> позволяет удалить совместно используемую память. Она бывает полезной, когда программа завершается аварийно при попытке убрать такую память.

Отображение состояния очереди сообщений

Для очередей сообщений предназначены команды ipcs -q и ipcrm -q <id> (или ipcrm msg <id>).

Далее приведен пример вывода команды ipcs -q:

$ ipcs -q

------ Message Queues ------

key        msqid owner perms used-bytes messages

0x000004d2 3384  rick  666   2048       2

В нем показаны в очереди сообщений два сообщения общим объемом 2048 байтов. Команда ipcrm -q <id> позволяет удалить очередь сообщений.

Резюме

В этой главе вы познакомились с тремя разновидностями средств взаимосвязи процессов, которые стали широко применяться в ОС UNIX System V.2 и были доступны в системе Linux, начиная с ранних версий ее дистрибутивов. Вы рассмотрели предлагаемые ими сложные функциональные возможности и, после того как поняли принципы их функционирования, оценили обеспечиваемое ими эффективное решение для удовлетворения многих потребностей межпроцессного взаимодействия. 

Глава 15

Сокеты

В этой главе вы познакомитесь с еще одним способом взаимодействия процессов, существенно отличающимся от тех, которые мы обсуждали в главах 13 и 14. До настоящего момента все рассматриваемые нами средства основывались на совместно используемых ресурсах одного компьютера. Ресурсы могли быть разными: областью файловой системы, сегментами совместно используемой памяти или очередями сообщений, но использовать их могли только процессы, выполняющиеся на одной машине.

В версию ОС Berkeley UNIX было включено новое средство коммуникации — интерфейс сокетов, — являющееся расширением концепции канала, обсуждавшейся в главе 13. В системах Linux также есть интерфейсы сокетов.

Вы можете применять сокеты во многом так же, как каналы, но они поддерживают взаимодействие в пределах компьютерной сети. Процесс на одной машине может использовать сокеты для взаимосвязи с процессом на другом компьютере, что делает возможным существование клиент-серверных систем, распределенных в сети. Процессы, выполняющиеся на одной машине, также могут применять сокеты.

Кроме того, интерфейс сокетов стал доступен в ОС Windows благодаря общедоступной спецификации Windows Sockets или WinSock. Сервисы сокетов в ОС Windows предоставляются системным файлом Winsock.dll. Стало быть, программы под управлением Windows могут взаимодействовать по сети с компьютерами под управлением Linux и UNIX и наоборот, реализуя, таким образом, клиент-серверные системы. Несмотря на то, что программный интерфейс для WinSock не совпадает полностью с интерфейсом сокетов в UNIX, в основе его лежат те же сокеты.

В одной-единственной главе мы не сможем дать исчерпывающее описание всех многообразных сетевых возможностей Linux, поэтому вы найдете здесь лишь основные программные сетевые интерфейсы, которые позволят вам писать собственные программы, работающие в сети.

Более подробно мы рассмотрим следующие темы:

□ как действует соединение с помощью сокетов;

□ атрибуты сокетов, адреса и обмен информацией;

□ сетевая информация и интернет-демон (inetd/xinetd);

□ клиенты и серверы.

Что такое сокет?

Сокет — это средство связи, позволяющее разрабатывать клиент-серверные системы для локального, на одной машине, или сетевого использования. Функции ОС Linux, такие как вывод, подключение к базам данных и обслуживание Web-страниц, равно как и сетевые утилиты, например rlogin, предназначенная для удаленной регистрации, и ftp, применяемая для передачи файлов, обычно используют сокеты для обмена данными.

Сокеты создаются и используются не так, как каналы, потому что они подчеркивают явное отличие между клиентом и сервером. Механизм сокетов позволяет создавать множество клиентов, присоединенных к единственному серверу.

Соединения на базе сокетов

Соединения на базе сокетов можно рассматривать как телефонные звонки в учреждение. Телефонный звонок поступает в организацию, и на него отвечает секретарь приемной, направляющий вызов в соответствующий отдел (серверный процесс) и оттуда к нужному сотруднику (сокет сервера). Каждый входящий телефонный звонок (клиент) направляется к соответствующей конечной точке, и промежуточные операторы могут заниматься последующими телефонными звонками. Прежде чем рассматривать установку соединений с помощью сокетов в системах Linux, нужно понять, как они ведут себя в приложениях сокетов, поддерживающих соединения.

Сначала серверное приложение создает сокет, который как файловый дескриптор представляет собой ресурс, присваиваемый единственному серверному процессу. Сервер создает его с помощью системного вызова socket, и этот сокет не может использоваться совместно с другими процессами.

Далее сервер присваивает сокету имя. Локальные сокеты с заданными именами файлов в файловой системе Linux часто размещаются в каталоге /tmp или /usr/tmp. У сетевых сокетов имя файла будет идентификатором сервиса (номер порта/точка доступа), относящегося к конкретной сети, к которой могут подключаться клиенты. Этот идентификатор, задавая определенный номер порта, соответствующий корректному серверному процессу, позволяет Linux направлять входящие подключения по определенному маршруту. Например, Web-сервер обычно создает сокет для порта 80, идентификатор, зарезервированный для этой цели. Web-обозреватели знают о необходимости применять порт 80 для своих HTTP-подключений к Web- сайтам, которые пользователь хочет читать. Именуется сокет с помощью системного вызова bind. Далее серверный процесс ждет подключения клиента к именованному сокету. Системный вызов listen формирует очередь входящих подключений. Сервер может принять их с помощью системного вызова accept.

Когда сервер вызывает accept, создается новый сокет, отличающийся от именованного сокета. Этот новый сокет применяется только для взаимодействия с данным конкретным клиентом. Именованный сокет сохраняется для дальнейших подключений других клиентов. Если сервер написан корректно, он может извлечь выгоду из многочисленных подключений. Web-сервер добивается этого за счет одновременного предоставления страниц многих клиентам. В случае простого сервера все последующие клиенты ждут в очереди до тех пор, пока сервер не будет готов снова.

Клиентская сторона системы с применением сокетов гораздо проще. Клиент создает неименованный сокет с помощью вызова socket. Затем он вызывает connect для подключения к серверу, используя в качестве адреса именованный сокет сервера.

Будучи установлены, сокеты могут применяться как низкоуровневые файловые дескрипторы, обеспечивая двунаправленный обмен данными.

Выполните упражнения 15.1 и 15.2.

Упражнение 15.1. Простой локальный клиент

Далее приведен пример очень простой клиентской программы client1.с. В ней неименованный сокет создается и затем подключается к сокету сервера, названному server_socket. Системный вызов socket мы подробно рассмотрим чуть позже, когда будем обсуждать некоторые проблемы адресации.

1. Включите нужные заголовочные файлы и задайте переменные:

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <sys/un.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int sockfd;

 int len;

 struct sockaddr_un address;

 int result;

 char ch = 'A';

2. Создайте сокет для клиента:

 sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

3. Назовите сокет по согласованию с сервером:

 address.sun_family = AF_UNIX;

 strcpy(address.sun_path, "server_socket");

 len = sizeof(address);

4. Соедините ваш сокет с сокетом сервера:

 result = connect(sockfd, (struct sockaddr *)&address, len);

 if (result == -1) {

  perror("oops : client1");

  exit(1);

 }

5. Теперь вы можете читать и писать через sockfd:

 write(sockfd, &ch, 1);

 read(sockfd, &ch, 1);

 printf("char from server = %c\n", ch);

 close(sockfd);

 exit(0);

}

Эта программа завершится аварийно, если вы попытаетесь выполнить ее, потому что еще не создан именованный сокет сервера, (Точное сообщение об ошибке может отличаться в разных системах.)

$ ./client1

oops: client1: No such file or directory

$

Упражнение 15.2. Простой локальный сервер

Далее приведена программа простого сервера server1.с, которая принимает запрос на соединение от клиента. Она создает сокет сервера, присваивает ему имя, создает очередь ожидания и принимает запросы на соединения.

1. Включите необходимые заголовочные файлы и задайте переменные:

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <sys/un.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int server_sockfd, client_sockfd;

 int server_len, client_len;

 struct sockaddr_un server_address;

 struct sockaddr_un client_address;

2. Удалите все старые сокеты и создайте неименованный сокет для сервера:

 unlink("server_socket");

 server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

3. Присвойте имя сокету:

 server_address.sun_family = AF_UNIX;

 strcpy(server_address.sun_path, "server_socket");

 server_len = sizeof(server_address);

 bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

4. Создайте очередь запросов на соединение и ждите запроса клиента:

 listen(server_sockfd, 5);

 while(1) {

  char ch;

  printf("server waiting\n");

5. Примите запрос на соединение:

  client_len = sizeof(client_address);

  client_sockfd = accept(server_sockfd,

   (struct sockaddr *)&client_address, &client_len);

6. Читайте и записывайте данные клиента с помощью client_sockfd:

  read(client_sockfd, &ch, 1);

  ch++;

  write(client_sockfd, &ch, 1);

  close(client_sockfd);

 }

}

Как это работает

В этом примере серверная программа в каждый момент времени может обслуживать только одного клиента. Она просто читает символ, поступивший от клиента, увеличивает его и записывает обратно. В более сложных системах, где сервер должен выполнять больше работы по поручению клиента, такой подход будет неприемлемым, потому что другие клиенты не смогут подключиться до тех пор, пока сервер не завершит работу. Позже вы увидите пару методов, позволяющих подключаться многочисленным клиентам.

Когда вы выполняете серверную программу, она создает сокет и ждет запросов на соединение. Если вы запустите ее в фоновом режиме, т.е. она будет выполняться независимо, вы сможете затем запускать клиентов как высокоприоритетные задачи.

$ ./server1 &

[1] 1094

$ server waiting

Ожидая запросы на соединения, сервер выводит сообщение. В приведенном примере сервер ждет запрос с сокета файловой системы, и вы сможете увидеть его с помощью обычной команды ls.

Хорошо взять за правило удалять сокет после окончания работы с ним, даже в случае аварийного завершения программы из-за получения сигнала. Это убережет файловую систему от загромождения неиспользуемыми файлами.

$ ls -lF server socket

srwxr-xr-x 1 neil users 0 2007-06-23 11:41 server_socket=

Здесь тип устройства — сокет, на что указывает символ s перед правами доступа и символ = в конце имени. Сокет был создан как обычный файл с правами доступа, модифицированными текущей umask. Если применить команду ps, то можно увидеть сервер, выполняющийся в фоновом режиме. Он показан спящим (параметр STAT равен s) и, следовательно, не потребляющим ресурсы ЦП.

$ ps lх

F  UID   PID  PPID PRI NI  VSZ RSS WCHAN  STAT TTY   TIME COMMAND

0 1000 23385 10689  17  0 1424 312 361800 S    pts/1 0:00 ./server1

Теперь, когда вы запустите программу, то успешно подключитесь к серверу. Поскольку сокет сервера существует, вы можете соединиться с ним и обмениваться данными.

$ ./client1

server waiting char from server = В

$

На терминале вывод сервера и клиента перемешаны, но можно увидеть, что сервер получил символ от клиента, увеличил его и вернул. Далее сервер продолжает выполняться и ждет следующего клиента. Если вы запустите несколько клиентов вместе, они будут обслуживаться по очереди, хотя полученный вывод может оказаться еще более перемешанным.

$ ./client1 & ./client1 & ./client1 &

[2] 23412

[3] 23413

[4] 23414

server waiting

char from server = В

server waiting

char from server = В

server waiting

char from server = В

server waiting

[2]  Done client1

[3]- Done client1

[4]+ Done client1

$

Атрибуты сокета

Для того чтобы до конца понять системные вызовы, применявшиеся в рассмотренном примере, необходимо узнать кое-что об организации сети в системах UNIX.

Сокеты характеризуются тремя атрибутами: доменом, типом и протоколом. У них также есть адрес, используемый как имя сокета. Форматы адресов меняются в зависимости от домена, также называемого семейством протоколов (protocol family). Каждое семейство протоколов может применять одно или несколько семейств адресов, определяющих формат адреса.

Домены сокетов

Домены задают сетевую рабочую среду, которую будет использовать соединение сокетов. Самый популярный домен сокетов — AF_INET, ссылающийся на сеть Интернет и применяемый во многих локальных сетях Linux и, конечно, в самом Интернете. Низкоуровневый протокол Internet Protocol (IP), у которого только одно адресное семейство, накладывает определенный способ задания компьютеров, входящих в сеть. Он называется IP-адресом.

Примечание

Для преодоления некоторых проблем стандартного протокола IP существенно ограниченного количества доступных адресов был разработан интернет-протокол нового поколения IPv6. Он использует другой домен сокетов AF_INET6 и иной формат адресов. Ожидается, что со временем IPv6 заменит IP, но для этого потребуется много лет. Несмотря на то, что уже есть реализации IPv6 для Linux, их обсуждение выходит за рамки этой книги.

Несмотря на то, что у машин в Интернете почти всегда есть имена, их преобразуют в IP-адреса. Пример IP-адреса — 192.168.1.99. Все IP-адреса представлены четырьмя числами, каждое из которых меньше 256, и образуют так называемые четверки с точками. Когда клиент подключается по сети с помощью сокетов, ему нужен IP- адрес компьютера сервера.

На компьютере сервера может быть доступно несколько сервисов. Клиент может обратиться к конкретному сервису на компьютере, включенном в сеть, с помощью IP-порта. Внутри системы порт идентифицируется уникальным 16-разрядным целым числом, а за пределами системы — комбинацией IP-адреса и номера порта. Сокеты — это коммуникационные конечные точки, которые должны быть связаны с портами, прежде чем передача данных станет возможна.

Серверы ожидают запросов на соединения от определенных клиентов. У хорошо известных сервисов есть выделенные номера портов, которые используются всеми машинами под управлением ОС Linux и UNIX. Обычно, но не всегда, эти номера меньше 1024. Примерами могут служить буфер печати принтера (515), rlogin (513), ftp (21) и httpd (80). Последний из названных — стандартный порт для Web-серверов. Обычно номера портов, меньшие 1024, зарезервированы для системных сервисов и могут обслуживаться процессами с правами суперпользователя. Стандарт X/Open определяет в заголовочном файле netdb.h константу IPPORT_RESERVED для указания наибольшего номера зарезервированных портов.

Поскольку для стандартных сервисов есть стандартный набор номеров портов, компьютеры могут легко соединяться друг с другом, не угадывая правильный номер порта. Локальный сервисы могут применять адреса нестандартных портов.

Домен в первом упражнении, AF_UNIX, — это домен файловой системы UNIX, который может использоваться сокетами, находящимися на единственном компьютере, возможно, даже не входящем в сеть. Если это так, то низкоуровневый протокол — это файловый ввод/вывод, а адреса — имена файлов. Для сокета сервера применялся адрес server_socket, который, как вы видели, появлялся в текущем каталоге, когда вы выполняли серверное приложение.

Кроме того, могут применяться и другие домены: AF_ISO для сетей на основе стандартных протоколов ISO и AF_XNS для Xerox Network System (сетевая система Xerox). В этой книге мы их не будем обсуждать.

Типы сокетов

У домена сокетов может быть несколько способов обмена данными, у каждого из которых могут быть разные характеристики. В случае сокетов домена AF_UNIX проблемы не возникают, т.к, они обеспечивают надежный двунаправленный обмен данными. В сетевых доменах необходимо знать характеристики базовой сети и их влияние на различные механизмы передачи данных.

Интернет-протоколы предоставляют два механизма передачи данных с разными уровнями обслуживания: потоки и дейтаграммы.

Потоковые сокеты

Потоковые сокеты (в чем-то подобные стандартным потокам ввода/вывода) обеспечивают соединение, представляющее собой последовательный и надежный двунаправленный поток байтов. Следовательно, гарантируется, что без указания возникшей ошибки данные не будут потеряны, продублированы или переупорядочены. Сообщения большого объема фрагментируются, передаются и снова собираются воедино. Это напоминает файловый поток, который принимает большие объемы данных и делит их на меньшие блоки для записи на физический диск. У потоковых сокетов предсказуемое поведение.

Потоковые сокеты, описываемые типом SOCK_STREAM, реализованы в домене AF_INET соединениями на базе протоколов TCP/IP. Кроме того, это обычный тип сокетов и в домене AF_UNIX. В этой главе мы сосредоточимся на сокетах типа SOCK_STREAM, поскольку они чаще всего применяются при программировании сетевых приложений.

Примечание

TCP/IP — сокращение для протоколов Transmission Control Protocol/Internet Protocol. Протокол IP — низкоуровневый протокол передачи пакетов, обеспечивающий выбор маршрута при пересылке данных в сети от одного компьютера к другому. Протокол TCP обеспечивает упорядочивание, управление потоком и ретрансляцию, гарантирующие полную и корректную передачу больших объемов данных или же сообщение о соответствующей ошибочной ситуации.

Дейтаграммные сокеты

В отличие от потоковых дейтаграммные сокеты, описываемые типом SOCK_DGRAM, не устанавливают и не поддерживают соединение. Кроме того, существует ограничение для размера дейтаграммы, которая может отправляться. Она передается как единое сетевое сообщение, которое может быть потеряно, продублировано или прибыть несвоевременно, т.е. перед дейтаграммами, посланными после нее.

Дейтаграммные сокеты реализованы в домене AF_INET с помощью соединений UDP/IP и предоставляют неупорядоченный ненадежный сервис. (UDP сокращенное название протокола User Datagram Protocol.) Однако они относительно экономичны с точки зрения расходования ресурсов, поскольку не нуждаются в поддержке сетевых соединений. Они быстры, т.к. не тратится время на установку сетевого соединения.

Дейтаграммы полезны для однократных запросов к информационным сервисам, для предоставления обычных сведений о состоянии или для выполнения низкоприоритетной регистрации данных. Их преимущество в том, что остановка сервера не причинит чрезмерных неудобств клиенту и не потребует перезапуска клиента. Поскольку серверы на базе дейтаграмм обычно сохраняют данные без соединения, их можно останавливать и запускать снова, не мешая их клиентам.

На этом мы закончим обсуждение дейтаграмм, дополнительную информацию см. в разд. "Дейтаграммы" в конце данной главы.

Протоколы сокетов

Если низкоуровневый механизм передачи данных позволяет применять несколько протоколов, предоставляющих сокет требуемого типа, можно выбрать конкретный протокол или сокет. В этой главе мы сосредоточимся на сокетах сети UNIX и ее файловой системы, которые не требуют от вас выбора протокола, отличного от заданного по умолчанию.

Создание сокета

Системный вызов socket создает сокет и возвращает дескриптор, который может применяться для доступа к сокету:

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

Созданный сокет — это одна конечная точка линии передачи. Параметр domain задает семейство адресов, параметр type определяет тип используемого с этим сокетом обмена данными, a protocol — применяемый протокол.

В табл. 15.1 приведены имена доменов.

Таблица 15.1

Домен Описание
AF_UNIX Внутренние для UNIX (сокеты файловой системы)
AF_INET Интернет-протоколы ARPA (Advanced Research Projects Agency, управление перспективных исследований и разработок) (сокеты сети UNIX)
AF_ISO Протоколы стандарта ISO (International Standards Organization, Международная организация по стандартизации)
AF_NS Протоколы сетевых систем Xerox
AF_IPX Novell-протокол IPX
AF_APPLETALK Appletalk DDS (Appletalk Digital Data Service)

К наиболее популярным доменам сокетов относятся AF_UNIX, применяемый для локальных сокетов, реализуемых средствами файловых систем UNIX и Linux, и AF_INET, используемый для сетевых сокетов UNIX. Сокеты домена AF_INET могут применяться программами, взаимодействующими в сетях на базе протоколов TCP/IP, включая Интернет. Интерфейс ОС Windows Winsock также предоставляет доступ к этому домену сокетов.

Параметр сокета type задает характеристики обмена данными, применяемые для нового сокета. Возможными значениями могут быть SOCK_STREAM и SOCK_DGRAM.

□ SOCK_STREAM — это упорядоченный, надежный, основанный на соединении, двунаправленный поток байтов. В случае домена сокетов AF_INET этот тип обмена данными по умолчанию обеспечивается TCP-соединением, которое устанавливается между двумя конечными точками потоковых сокетов при подключении. Данные могут передаваться в двух направлениях по линии связи сокетов. Протоколы TCP включают в себя средства фрагментации и последующей повторной сборки сообщений больших объемов и повторной передачи любых их частей, которые могли быть потеряны в сети.

□ SOCK_DGRAM — дейтаграммный сервис. Вы можете использовать такой сокет для отправки сообщений с фиксированным (обычно небольшим) максимальным объемом, но при этом нет гарантии, что сообщение будет доставлено или что сообщения не будут переупорядочены в сети. В случае сокетов домена AF_INET этот тип передачи данных обеспечивается дейтаграммами UDP (User Datagram Protocol, пользовательский протокол дейтаграмм).

Протокол, применяемый для обмена данными, обычно определяется типом сокета и доменом. Как правило, выбора нет. Параметр protocol применяется в тех случаях, когда выбор все же предоставляется. Задание 0 позволяет выбрать стандартный протокол, используемый во всех примерах данной главы.

Системный вызов socket возвращает дескриптор, во многом похожий на низкоуровневый файловый дескриптор. Когда сокет подключен к концевой точке другого сокета, для отправки и получения данных с помощью сокетов можно применять системные вызовы read и write с дескриптором сокета. Системный вызов close используется для удаления сокетного соединения.

Адреса сокетов

Каждый домен сокетов требует своего формата адресов. В домене AF_UNIX адрес описывается структурой sockaddr_un, объявленной в заголовочном файле sys/un.h: 

struct sockaddr_un {

 sa_family_t sun_family; /* AF_UNIX */

 char sun_path[];        /* Путь к файлу */

};

Для того чтобы адреса разных типов могли передаваться в системные вызовы для обработки сокетов, все адресные форматы описываются похожей структурой, которая начинается с поля (в данном случае sun_family), задающего тип адреса (домен сокета). В домене AF_UNIX адрес задается именем файла в поле структуры sun_path.

В современных системах Linux тип sa_family_t, описанный в стандарте X/Open как объявляемый в заголовочном файле sys/un.h, интерпретируется как тип short. Кроме того, размер pathname, задаваемого в поле sun_path, ограничен (в Linux указывается 108 символов; в других системах может применяться именованная константа, например, UNIX_MAX_PATH). Поскольку размер адресной структуры может меняться, многие системные вызовы сокетов требуют или предоставляют на выходе длину, которая будет использоваться для копирования конкретной адресной структуры.

В домене AF_INET адрес задается с помощью структуры с именем sockaddr_in, определенной в файле netinet/in.h, которая содержит как минимум следующие элементы:

struct sockaddr_in {

 short int sin_family;        /* AF_INET */

 unsigned short int sin_port; /* Номер порта */

 struct in_addr sin_addr;     /* Интернет-адрес */

};

Структура IP-адреса типа in_addr определена следующим образом:

struct in_addr {

 unsigned long int s_addr;

};

Четыре байта IP-адреса образуют одно 32-разрядное значение. Сокет домена AF_INET полностью описывается IP-адресом и номером порта. С точки зрения приложения все сокеты действуют как файловые дескрипторы, и их адреса задаются уникальными целочисленными значениями.

Именование сокета

Для того чтобы сделать сокет (созданный с помощью вызова socket) доступным для других процессов, серверная программа должна присвоить сокету имя. Сокеты домена AF_UNIX связаны с полным именем файла в файловой системе, как вы видели в программе-примере server1. Сокеты домена AF_INET связаны с номером IP-порта.

#include <sys/socket.h>

int bind(int socket, const struct sockaddr *address, size_t address len);

Системный вызов bind присваивает адрес, заданный в параметре address, неименованному сокету, связанному с дескриптором сокета socket. Длина адресной структуры передается в параметре address_len:

Длина и формат адреса зависят от адресного семейства. В системном вызове bind указатель конкретной адресной структуры должен быть приведен к обобщенному адресному типу (struct sockaddr*).

В случае успешного завершения bind возвращает 0. Если он завершается аварийно, возвращается -1, и переменной errno присваивается одно из значений, перечисленных в табл. 15.2.

Таблица 15.2

Значение errno Описание
EBADF Неверный файловый дескриптор
ENOTSOCK Файловый дескриптор не ссылается на сокет
EINVAL Файловый дескриптор ссылается на сокет, уже получивший имя
EADDRNOTAVAIL Недопустимый адрес
EADDINUSE У адреса уже есть связанный с ним сокет
Для сокетов домена AF_UNIX есть несколько дополнительных значений
EACCESS Невозможно создать имя в файловой системе из-за прав доступа
ENOTDIR, ENAMETOOLONG Означает недопустимое имя файла

Создание очереди сокетов

Для приема запросов на входящие соединения на базе сокетов серверная программа должна создать очередь для хранения ждущих обработки запросов. Формируется она с помощью системного вызова listen.

#include <sys/socket.h>

int listen(int socket, int backlog);

Система Linux может ограничить количество ждущих обработки соединений, которые могут храниться в очереди. В соответствии с этим максимумом вызов listen задает длину очереди, равной backlog. Входящие соединения, не превышающие максимальной длины очереди, сохраняются в ожидании сокета; последующим запросам на соединение будет отказано, и клиентская попытка соединения завершится аварийно. Этот механизм реализуется вызовом listen для того, чтобы можно было сохранить ждущие соединения запросы, пока серверная программа занята обработкой запроса предыдущего клиента. Очень часто параметр backlog равен 5.

Функция listen вернет 0 в случае успешного завершения и -1 в случае ошибки. Как и для системного вызова bind, ошибки могут обозначаться константами EBADF, EINVAL И ENOTSOCK.

Прием запросов на соединение

После создания и именования сокета серверная программа может ждать запросы на выполнение соединения с сокетом с помощью системного вызова accept:

#include <sys/socket.h>

int accept(int socket, struct sockaddr *address, size_t *address_len);

Системный вызов accept возвращает управление, когда клиентская программа пытается подключиться к сокету, заданному в параметре socket. Этот клиент — первый из ждущих соединения в очереди данного сокета. Функция accept создает новый сокет для обмена данными с клиентом и возвращает его дескриптор. У нового сокета будет тот же тип, что и у сокета сервера, ждущего запросы на соединения.

Предварительно сокету должно быть присвоено имя с помощью системного вызова bind и у него должна быть очередь запросов на соединение, место для которой выделил системный вызов listen. Адрес вызывающего клиента будет помещен в структуру sockaddr, на которую указывает параметр address. Если адрес клиента не представляет интереса, в этом параметре может задать пустой указатель.

Параметр address_len задает длину адресной структуры клиента. Если адрес клиента длиннее, чем это значение, он будет урезан. Перед вызовом accept в параметре address_len должна быть задана ожидаемая длина адреса. По возвращении из вызова в address_len будет установлена реальная длина адресной структуры запрашивающего соединение клиента.

Если нет запросов на соединение, ждущих в очереди сокета, вызов accept будет заблокирован (так что программа не сможет продолжить выполнение) до тех пор, пока клиент не сделает запрос на соединение. Вы можете изменить это поведение, применив флаг O_NONBLOCK в файловом дескрипторе сокета с помощью вызова fcntl в вашей программе следующим образом:

int flags = fcntl(socket, F_GETFL, 0);

fcntl(socket, F_SETFL, O_NONBLOCK | flags);

Функция accept возвращает файловый дескриптор нового сокета, если есть запрос клиента, ожидающего соединения, и -1 в случае ошибки. Возможные значения ошибок такие же, как у вызовов bind и listen плюс дополнительная константа EWOULDBLOCK в случае, когда задан флаг O_NONBLOCK и нет ждущих запросов на соединение. Ошибка EINTR возникнет, если процесс прерван во время блокировки в функции accept.

Запросы соединений

Клиентские программы подключаются к серверам, устанавливая соединение между неименованным сокетом и сокетом сервера, ждущим подключений. Делают они это с помощью вызова connect:

#include <sys/socket.h>

int connect(int socket, const struct sockaddr *address, size_t address_len);

Сокет, заданный в параметре socket, соединяется с сокетом сервера, заданным в параметре address, длина которого равна address_len. Сокет должен задаваться корректным файловым дескриптором, полученным из системного вызова socket.

Если функция connect завершается успешно, она возвращает 0, в случае ошибки вернется -1. Возможные ошибки на этот раз включают значения, перечисленные в табл. 15.3.

Таблица 15.3

Значение errno Описание
EBADF В параметре socket задан неверный файловый дескриптор
EALREADY Для этого сокета соединение уже обрабатывается
ETIMEDOUT Допустимое время ожидания соединения превышено
ECONNREFUSED Запрос на соединение отвергнут сервером

Если соединение не может быть установлено немедленно, вызов connect будет заблокирован на неопределенный период ожидания. Когда допустимое время ожидания будет превышено, соединение разорвется и вызов connect завершится аварийно. Однако, если вызов прерван сигналом, который обрабатывается, connect завершится аварийно (со значением errno, равным EINTR), но попытка соединения не будет прервана — соединение будет установлено асинхронно и программа должна будет позже проверить, успешно ли оно установлено.

Как и в случае вызова accept, возможность блокировки в вызове connect можно исключить установкой в файловом дескрипторе флага O_NONBLOCK. В этом случае, если соединение не может быть установлено немедленно, вызов connect завершится аварийно с переменной errno, равной EINPROGRESS, и соединение будет выполнено асинхронно.

Хотя асинхронные соединения трудно обрабатывать, вы можете применить вызов select к файловому дескриптору сокета, чтобы убедиться в том, что сокет готов к записи. Мы обсудим вызов select чуть позже в этой главе.

Закрытие сокета

Вы можете разорвать сокетное соединение в серверной или клиентской программах, вызвав функцию close, так же как в случае низкоуровневых файловых дескрипторов. Сокеты следует закрывать на обоих концах. На сервере это нужно делать, когда read вернет ноль. Имейте в виду, что вызов close может быть заблокирован, если сокет, у которого есть непереданные данные, обладает типом, ориентированным на соединение, и установленным параметром SOCK_LINGER. Дополнительную информацию об установке параметров сокета вы узнаете позже в этой главе.

Обмен данными с помощью сокетов

Теперь, когда мы описали основные системные вызовы, связанные с сокетами, давайте повнимательнее рассмотрим программы-примеры. Вы попытаетесь переработать их, заменив сокет файловой системы сетевым сокетом. Недостаток сокета файловой системы состоит в том, что если автор не использует полное имя файла, он создается в текущем каталоге серверной программы. Для того чтобы сделать его полезным в большинстве случаев, следует создать сокет в общедоступном каталоге (например, /tmp), подходящем для сервера и его клиентов. В случае сетевых серверов достаточно выбрать неиспользуемый номер порта.

Для примера выберите номер порта 9734. Это произвольный выбор, позволяющий избежать использования портов стандартных сервисов (вы не должны применять номера портов, меньшие 1024, поскольку они зарезервированы для системного использования). Другие номера портов с обеспечиваемыми ими сервисами часто приводятся в системном файле /etc/services. При написании программ, использующих сокеты, всегда выбирайте номер порта, которого нет в этом файле конфигурации.

Примечание

Вам следует знать, что в программах client2.c и server2.c умышленно допущена ошибка, которую вы устраните в программах client3.c и server3.c. Пожалуйста, не используйте текст примеров client2.c и server2.c в собственных программах.

Вы будете выполнять ваши серверную и клиентскую программы в локальной сети, но сетевые сокеты полезны не только в локальной сети, любая машина с подключением к Интернету (даже по модемной линии связи) может применять сетевые сокеты для обмена данными с другими компьютерами. Программу, основанную на сетевых подключениях, можно применять даже на изолированном компьютере с ОС UNIX, т. к. такой компьютер обычно настроен на использование виртуальной сети или внутренней петли (loopback network), включающей только его самого. Для демонстрационных целей данный пример использует виртуальную сеть, которая может быть также полезна для отладки сетевых приложений, поскольку она устраняет любые внешние сетевые проблемы.

Виртуальная сеть состоит из единственного компьютера, традиционно именуемого localhost, со стандартным IP-адресом 127.0.0.1. Это локальная машина. Ее адрес вы сможете найти в файле сетевых узлов etc/hosts наряду с именами и адресами других узлов, входящих в совместно используемые сети.

У каждой сети, с которой компьютер обменивается данными, есть связанный с ней аппаратный интерфейс. У компьютера в каждой сети может быть свое имя и конечно будут разные IP-адреса. Например, у машины Нейла с именем tilde три сетевых интерфейса и, следовательно, три адреса. Они записаны в файле /etc/hosts следующим образом.

127.0.0.1    localhost         # Петля

192.168.1.1  tilde.localnet    # Локальная частная сеть Ethernet

158.152.X.X  tilde.demon.co.uk # Модемная линия связи

Первая строка — пример виртуальной сети, ко второй сети доступ осуществляется с помощью адаптера Ethernet, а третья — модемная линия связи с провайдером интернет-сервисов. Вы можете написать программу, применяющую сетевые сокеты, для связи с серверами с помощью любого из приведенных интерфейсов без каких-либо корректировок.

Выполните упражнения 15.3 и 15.4.

Упражнение 15.3. Сетевой клиент

Далее приведена измененная программа-клиент client2.c, предназначенная для использования сетевого соединения на базе сокета в виртуальной сети. Она содержит незначительную ошибку, связанную с аппаратной зависимостью, но мы обсудим ее чуть позже в этой главе.

1. Включите необходимые директивы #include и задайте переменные:

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int sockfd;

 int len;

 struct sockaddr_in address;

 int result;

 char ch = 'A';

2. Создайте сокет клиента:

 sockfd = socket(AF_INET, SOCK_STREAM, 0);

3. Присвойте имя сокету по согласованию с сервером:

 address.sin_family = AF_INET;

 address.sin_addr.s_addr = inet_addr("127.0.0.1");

 address.sin_port = 9734;

 len = sizeof(address);

Оставшаяся часть программы такая же, как в приведенном ранее в этой главе примере. Когда вы выполните эту версию, она завершится аварийно, потому что на данном компьютере нет сервера, выполняющегося на порте 9734.

$ ./client2

oops: client2: Connection refused

$

Как это работает

Клиентская программа использует структуру sockaddr_in из заголовочного файла netinet/in.h для задания адреса AF_INET. Она пытается подключиться к серверу, размещенному на узле с IP-адресом 127.0.0.1. Программа применяет функцию inet_addr для преобразования текстового представления IP-адреса в форму, подходящую для адресации сокетов. На страницах интерактивного справочного руководства для inet вы найдете дополнительную информацию о других функциях, преобразующих адреса.

Упражнение 15.4. Сетевой сервер

Вам также нужно модифицировать серверную программу, ждущую подключений на выбранном вами номере порта. Далее приведена откорректированная программа сервера server2.c.

1. Вставьте необходимые заголовочные файлы и задайте переменные:

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int server_sockfd, client_sockfd;

 int server_len, client_len;

 struct sockaddr_in server_address;

 struct sockaddr_in client_address;

2. Создайте неименованный сокет для сервера:

 server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

3. Дайте имя сокету:

 server_address.sin_family = AF_INET;

 server_address.sin_port.s_addr = inet_addr("127.0.0.1");

 server_address.sin_port = 9734;

 server_len = sizeof(server_address);

 bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

С этой строки и далее текст примера точно совпадает с программным кодом в файле server1.c. Выполнение client2 и server2 продемонстрирует то же поведение, что и при запуске программ client1 и server1.

Как это работает

Серверная программа создает сокет домена AF_INET и выполняет необходимые действия для приема запросов на подключение к нему. Сокет связывается с выбранным вами портом. Заданный адрес определяет, каким машинам разрешено подсоединяться. Задавая такой же адрес виртуальной сети, как в клиентской программе, вы ограничиваете соединения только локальной машиной.

Если вы хотите разрешить серверу устанавливать соединения с удаленными клиентами, необходимо задать набор IP-адресов, которые разрешены. Можно применить специальное значение INADDR_ANY для того, чтобы показать, что будете принимать запросы на подключение от всех интерфейсов, имеющихся на вашем компьютере. Если необходимо, вы можете разграничить интерфейсы разных сетей, чтобы отделить соединения локальной сети от соединений глобальной сети. Константа INADDR_ANY — 32-разрядное целое число, которое можно использовать в поле sin_addr.s_addr адресной структуры. Но прежде вам нужно решить проблему.

Порядок байтов на компьютере и в сети

Если запустить приведенные версии серверной и клиентской программ на машине на базе процессора Intel под управлением Linux, то с помощью команды netstat можно увидеть сетевые соединения. Эта команда есть в большинство систем UNIX, настроенных на работу в сети. Она отображает клиент-серверное соединение, ожидающее закрытия. Соединение закрывается после небольшой задержки. (Повторяем, что вывод в разных версиях Linux может отличаться.)

$ ./server2 & ./client2

[3] 23770

server waiting

server waiting

char from server = В

$ netstat -A inet

Active Internet connections (w/o servers)

Proto Recv-Q Send-Q Local Address  Foreign Address (State)   User

tcp        1      0 localhost:1574 localhost:1174  TIME_WAIT root

Примечание

Прежде чем испытывать последующие примеры этой главы, убедитесь в том, что завершено выполнение серверных программ-примеров, поскольку они будут конкурировать при приеме соединений клиентов, и вы увидите вводящие в заблуждение результаты. Удалить их все (включая те, что будут приведены позже в этой главе) можно с помощью следующей команды:

killall server1 server2 server3 server4 server5

Вы сможете увидеть номера портов, присвоенные соединению сервера с клиентом. Локальный адрес отображает сервер, а внешний адрес — удаленного клиента. (Даже если клиент размещен на той же машине, он все равно подключается через сеть.) Для четкого разделения всех сокетов порты клиентов обычно отличаются от сокета сервера, ожидающего запросы на соединения, и уникальны в пределах компьютера.

Отображается локальный адрес (сокет сервера) 1574 (или может выводиться имя сервиса mvel-lm) и выбранный в примере порт 9734. Почему они отличаются? Дело в том, что номера портов и адреса передаются через интерфейсы сокета как двоичные числа. В разных компьютерах применяется различный порядок байтов для представления целых чисел. Например, процессор Intel хранит 32-разрядное целое в виде четырех последовательных байтов памяти в следующем порядке 1-2-3-4, где 1-й байт — самый старший. Процессоры IBM PowerPC будут хранить целое со следующим порядком следования байтов: 4-3-2-1. Если используемую для хранения целых память просто побайтно копировать, два компьютера не придут к согласию относительно целочисленных значений.

Для того чтобы компьютеры разных типов могли согласовать значения многобайтовых целых чисел, передаваемых по сети, необходимо определить сетевой порядок передачи байтов. Перед передачей данных клиентские и серверные программы должны преобразовать собственное внутреннее представление целых чисел в соответствии с принятым в сети порядком следования байтов. Делается это с помощью функций, определенных в заголовочном файле netinet/in.h. К ним относятся следующие:

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);

unsigned short int htons(unsigned short int hostshort);

unsigned long int ntohl(unsigned long int netlong);

unsigned short int ntohs(unsigned short int netshort);

Эти функции преобразуют 16- и 32-разрядные целые из внутреннего формата в сетевой порядок следования байтов и обратно. Их имена соответствуют сокращенному названию выполняемых преобразований, например "host to network, long" (htonl, компьютерный в сетевой, длинные целые) и "host to network, short" (htons, компьютерный в сетевой, короткие целые). Компьютерам, у которых порядок следования байтов соответствует сетевому, эти функции предоставляют пустые операции.

Для обеспечения корректного порядка следования при передаче 16-разрядного целого числа ваши сервер и клиент должны применить эти функции к адресу порта. В программу server3.c следует внести следующие изменения:

server_address.sin_addr_s_addr = htonl(INADDR_ANY);

server_address.sin_port = htons(9734);

Результат, возвращаемый функцией inet_addr("127.0.0.1"), преобразовывать не нужно, потому что в соответствии со своим определением она возвращает результат с сетевым порядком следования байтов. В программу client3.c необходимо внести следующее изменение:

address.sin_port = htons(9734);

В сервер, благодаря применению константы INADDR_ANY, внесено изменение, позволяющее принимать запросы на соединение от любых IP-адресов.

Теперь, выполнив программы server3 и client3, вы увидите корректный номер порта, используемый для локального соединения:

$ netstat

Active Internet connections

Proto Recv-Q Send-Q Local Address  Foreign Address (State)   User

tcp        1      0 localhost:9734 localhost:1175  TIME_WAIT root

Примечание

Если вы пользуетесь компьютером, у которого собственный формат представления целых совпадает с сетевым порядком следования байтов, вы не увидите никакой разницы. Но для обеспечения корректного взаимодействия клиентов и серверов с разной архитектурой важно всегда применять функции преобразования.

Сетевая информация

До сих пор у клиентских и серверных программ были адреса и номера портов, компилируемые в них. В более универсальных серверных и клиентских программах для определения применяемых адресов и портов вы можете использовать данные сети.

Если у вас есть на это право, можно добавить свой сервер к списку известных сервисов в файл /etc/services, который назначает имена номерам портов, так что клиенты могут использовать вместо номеров символические имена сервисов.

Точно так же зная имя компьютера, можно определить IP-адрес, вызвав функции базы данных сетевых узлов (host database), которые найдут эти адреса. Делают они это, обращаясь за справкой к конфигурационным файлам, например, etc/hosts или к сетевым информационным сервисам, таким как NIS (Network Information Services (сервисы сетевой информации), ранее известным как Yellow Pages (желтые страницы)) и DNS (Domain Name Service, служба доменных имен).

Функции базы данных сетевых узлов или хостов (Host database) объявлены в заголовочном файле интерфейса netdb.h:

#include <netdb.h>

struct hostent *gethostbyaddr(const void* addr, size_t len, int type);

struct hostent* gethostbyname(const char* name);

Структура, возвращаемая этими функциями, должна как минимум содержать следующие элементы.

struct hostent {

 char *h_name;      /* Имя узла */

 char **h_aliases;  /* Перечень псевдонимов (nicknames) */

 int h_addrtype;    /* Тип адреса */

 int h_length;      /* Длина адреса в байтах */

 char **h_addr_list /* Перечень адреса (сетевой порядок байтов) */

};

Если в базе данных нет элемента, соответствующего заданному узлу или адресу, информационные функции вернут пустой указатель.

Аналогично информацию, касающуюся сервисов и связанных номеров портов, можно получить с помощью информационных функций сервисов:

#include <netdb.h>

struct servent *getservbyname(const char *name, const char *proto);

struct servent *getservbyport(int port, const char *proto);

Параметр proto задает протокол, который будет применяться для подключения к сервису, либо "tcp" для TCP-соединений типа SOCK_STREAM, либо "udp" для UDP-дейтаграмм типа SOCK_DGRAM.

Структура servent содержит как минимум следующие элементы:

struct servent {

 char *s_name;     /* Имя сервиса */

 char **s_aliases; /* Список псевдонимов (дополнительных имен) */

 int s_port;       /* Номер IP-порта */

 char *s_proto;    /* Тип сервиса, обычно "tcp" или "udp" */

}

Вы можете собрать воедино информацию о компьютере из базы данных сетевых узлов, вызвав функцию gethostbyname и выведя ее результаты. Учтите, что адрес необходимо преобразовать в соответствующий тип и перейти от сетевого упорядочивания к пригодной для вывода строке с помощью преобразования inet_ntoa, определенного следующим образом:

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);

Функция преобразует адрес интернет-узла в строку формата четверки чисел с точками. В случае ошибки она возвращает -1, но в стандарте POSIX не определены конкретные ошибки. Еще одна новая функция, которую вы примените, — gethostname:

#include <unistd.h>

int gethostname(char *name, int name length);

Эта функция записывает имя текущего узла в строку, заданную параметром name. Имя узла будет нуль-терминированной строкой. Аргумент namelength содержит длину строкового имени и, если возвращаемое имя узла превысит эту длину, оно будет обрезано. Функция gethostname возвращает 0 в случае успешного завершения и -1 в случае ошибки. И снова ошибки в стандарте POSIX не определены.

Выполните упражнение 15.5.

Упражнение 15.5. Сетевая информация

Данная программа getname.c получает сведения о компьютере.

1. Как обычно, вставьте соответствующие заголовочные файлы и объявите переменные:

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <netdb.h>

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char *argv[]) {

 char *host, **names, **addrs;

 struct hostent *hostinfo;

2. Присвойте переменной host значение аргумента, предоставляемого при вызове программы getname, или по умолчанию имя машины пользователя:

 if (argc == 1) {

  char myname[256];

  gethostname(myname, 255);

  host = myname;

 } else host = argv[1];

3. Вызовите функцию gethostbyname и сообщите об ошибке, если никакая информация не найдена:

 hostinfo = gethostbyname(host);

 if (!hostinfo) {

  fprintf(stderr, "cannot get info for host: %s\n", host);

  exit(1);

 }

4. Отобразите имя узла и любые псевдонимы, которые у него могут быть:

 printf("results for host %s:\n", host);

 printf("Name : %s\n", hostinfo->h_name);

 printf("Aliases: ");

 names = hostinfo->h_aliases;

 while (*names) {

  printf(" %s", *names); names++;

 }

 printf("\n");

5. Если запрашиваемый узел не является IP-узлом, сообщите об этом и завершите выполнение:

 if (hostinfo->h_addrtype != AF_INET) {

  fprintf(stderr, "not an IP host!\n");

  exit(1);

 }

6. В противном случае выведите IP-адрес (адреса):

 addrs = hostinfo->h_addr_list;

 while (*addrs) {

  printf(" %s", inet_ntoa(*(struct in_addr*)*addrs));

  addrs++;

 }

 printf("\n");

 exit(0);

}

Для определения узла по заданному IP-адресу можно применить функцию gethostbyaddr. Вы можете использовать ее на сервере для того, чтобы выяснить, откуда клиент запрашивает соединение.

Как это работает

Программа getname вызывает функцию gethostbyname для извлечения сведений об узле из базы данных сетевых узлов. Она выводит имя компьютера, его псевдонимы (другие имена, под которыми известен компьютер) и IP-адреса, которые он использует в своих сетевых интерфейсах. На одной из машин авторов выполнение примера и указание в качестве аргумента имени tilde привело к выводу двух интерфейсов: сети Ethernet и модемной линии связи.

$ ./getname tilde

results for host tilde:

Name: tilde.localnet

Aliases: tilde

192.168.1.1 158.152.x.x

Когда используется имя узла localhost, задается виртуальная сеть:

$ ./getname localhost

results for host localhost:

Name: localhost

Aliases: 127.0.0.1

Теперь вы можете изменить свою программу-клиента для соединения с любым именованным узлом сети. Вместо подключения к серверу из вашего примера, вы соединитесь со стандартным сервисом и сможете извлечь номер порта.

Большинство систем UNIX и некоторые ОС Linux делают доступными свои системные время и дату в виде стандартного сервиса с именем daytime. Клиенты могут подключаться к этому сервису для выяснения мнения сервера о текущих времени и дате. В упражнении 15:6 приведена программа-клиент getdate.c, именно это и делающая.

Упражнение 15.6. Подключение к стандартному сервису

1. Начните с обычных директив #include и объявлений:

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main(int argc, char *argv[]) {

 char *host;

 int sockfd;

 int len, result;

 struct sockaddr_in address;

 struct hostent *hostinfo;

 struct servent *servinfo;

 char buffer[128];

 if (argc == 1) host = "localhost";

 else host = argv[1];

2. Найдите адрес узла и сообщите об ошибке, если адрес не найден:

 hostinfo = gethostbyname(host);

 if (!host info) {

  fprintf(stderr, "no host: %s\n", host);

  exit(1);

 }

3. Убедитесь, что на компьютере есть сервис daytime:

 servinfo = getservbyname("daytime", "tcp");

 if (!servinfo) {

  fprintf(stderr, "no daytime service\n");

  exit(1);

 }

 printf("daytime port is %d\n", ntohs(servinfo->s_port));

4. Создайте сокет:

 sockfd = socket(AF_INET, SOCK_STREAM, 0);

5. Сформируйте адрес для соединения:

 address.sin_family = AF_INET;

 address.sin_port = servinfo->s_port;

 address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list;

 len = sizeof(address);

6. Затем подключитесь и получите информацию:

 result = connect(sockfd, (struct sockaddr *)&address, len);

 if (result == -1) {

  perror("oops: getdate");

  exit(1);

 }

 result = read(sockfd, buffer, sizeof(buffer));

 buffer[result] = '\0';

 printf("read %d bytes: %s", result, buffer);

 close(sockfd);

 exit(0);

}

Вы можете применять программу getdate для получения времени суток с любого известного узла сети.

$ ./getdate localhost

daytime port is 13

read 26 bytes: 24 JUN 2007 06:03:03 BST

$

Если вы получаете сообщение об ошибке, такое как

oops: getdate: Connection refused

или

oops: getdate: No such file or directory

причина может быть в том, что на компьютере, к которому вы подключаетесь, не включен сервис daytime. Такое поведение стало стандартным для большинства современных систем Linux. В следующем разделе вы увидите, как включать этот и другие сервисы.

Как это работает

При выполнении данной программы можно задать узел, к которому следует подключиться. Номер порта сервиса daytime определяется функцией сетевой базы данных getservbyname, которая возвращает сведения о сетевых сервисах таким же способом, как и при получении информации об узле сети. Программа getdate пытается соединиться с адресом, который указан первым в списке дополнительных адресов заданного узла. Если соединение успешно, программа считывает сведения, возвращаемые сервисом daytime, символьную строку, содержащую системные дату и время.

Интернет-демон (xinetd/inetd)

Системы UNIX, предоставляющие ряд сетевых сервисов, зачастую делают это с помощью суперсервера. Эта программа (интернет-демон xinetd или inetd) ожидает одновременно запросы на соединения с множеством адресов портов. Когда клиент подключается к сервису, программа-демон запускает соответствующий сервер. При таком подходе серверам не нужно работать постоянно, они могут запускаться по требованию.

Примечание

В современных системах Linux роль интернет-демона исполняет программа xinetd. Она заменила оригинальную UNIX-программу inetd, которую вы все еще можете встретить в более ранних системах Linux и других UNIX-подобных системах.

Программа xinetd обычно настраивается с помощью пользовательского графического интерфейса для управления сетевыми сервисами, но вы можете изменять и непосредственно файлы конфигурации программы. К ним относятся файл /etc/xinetd.conf и файлы в каталоге /etc/xinetd.d.

У каждого сервиса, предоставляемого программой xinetd, есть файл конфигурации в каталоге /etc/xinetd.d. Программа xinetd считает все эти файлы конфигурации во время запуска и повторно при получении соответствующей команды.

Далее приведена пара примеров файлов конфигурации xinetd, первый из них для сервиса daytime.

# По умолчанию: отключен

# Описание: сервер daytime. Это версия tcp.

service daytime

{

 socket_type = stream

 protocol    = tcp

 wait        = no

 user        = root

 type        = INTERNAL

 id          = daytime-stream

 FLAGS       = IPv6 IPv4

}

Следующий файл конфигурации предназначен для сервиса передачи файлов.

# По умолчанию: отключен

# Описание:

# FTP-сервер vsftpd обслуживает FTP-соединения. Он использует

# для аутентификации обычные, незашифрованные имена пользователей и

# пароли, vsftpd спроектирован для безопасной работы.

#

# Примечание: этот файл содержит конфигурацию запуска vsftpd для xinetd.

# Файл конфигурации самой программы vsftpd находится в

# /etc/vsftpd.conf

service ftp {

# server_args =

# log_on_success += DURATION USERID

# log_on_failure += USERID

# nice = 10

 socket_type = stream

 protocol    = tcp

 wait        = no

 user        = root

 server      = /usr/sbin/vsftpd

}

Сервис daytime, к которому подключается программа getdate, обычно обрабатывается самой программой xinetd (он помечен как внутренний) и может включаться с помощью как сокетов типа SOCK_STREAM (tcp), так и сокетов типа SOCK_DGRAM (udp).

Сервис передачи файлов ftp подключается только сокетами типа SOCK_STREAM и предоставляется внешней программой, в данном случае vsftpd. Демон будет запускать эту внешнюю программу, когда клиент подключится к порту ftp.

Для активизации конфигурационных изменений сервиса можно отредактировать конфигурацию xinetd и отправить сигнал отбоя (hang-up) процессу-демону, но мы рекомендуем использовать более дружелюбный способ настройки сервисов. Для того чтобы разрешить вашему клиенту подключаться к сервису daytime, включите этот сервис с помощью средств, предоставляемых системой Linux. В системах SUSE и openSUSE сервисы можно настраивать из SUSE Control Center (Центр управления SUSE), как показано на рис. 15.1. У версий Red Hat (и Enterprise Linux, и Fedora) есть похожий интерфейс настройки. В нем сервис daytime включается для TCP- и UDP-запросов.

Рис. 15.1 

Для систем, применяющих программу inetd вместо xinetd, далее приведено эквивалентное извлечение из файла конфигурации inetd, /etc/inetd.conf, которое программа inetd использует для принятия решения о запуске серверов:

#

# <service_name> <sock_type> <proto> <flags> <user> <server_path> <args>

#

# Echo, discard, daytime и chargen используются в основном для

# тестирования.

#

daytime stream tcp nowait root internal

daytime dgram udp wait root internal

#

# Это стандартные сервисы.

#

ftp stream tcp-nowait root /usr/sbin/tcpd /usr/sbin/wu.ftpd

telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/in.telnetd

#

# Конец файла inetd.conf.

Обратите внимание на то, что в нашем примере сервис ftp предоставляется внешней программой wu.ftpd. Если в вашей системе выполняется демон inetd, вы можете изменить набор предоставляемых сервисов, отредактировав файл /etc/inetd.conf (знак # в начале строки указывает на то, что это строка комментария) и перезапустив процесс inetd. Сделать это можно, отправив сигнал отбоя (hang-up) с помощью команды kill. Для облегчения этого процесса некоторые системы настроены так, что программа inetd записывает свой ID в файл. В противном случае можно применить команду killall:

# killall -HUP inetd

Параметры сокета

Существует много параметров, которые можно применять для управления поведением соединений на базе сокетов — слишком много для подробного описания в этой главе. Для манипулирования параметрами используют функцию setsockopt:

#include <sys/socket.h>

int setsockopt(int socket, int level, int option_name,

 const void *option value, size_t option len);

Задавать параметры можно на разных уровнях иерархии протоколов. Для установки параметров на уровне сокета вы должны задать level равным SOL_SOCKET. Для задания параметров на более низком уровне протоколов (TCP, UDP и т.д.) приравняйте параметр level номеру протокола (полученному либо из заголовочного файла netinet/in.h, либо из функции getprotobyname).

В аргументе option_name указывается имя задаваемого параметра, аргумент option_value содержит произвольное значение длиной option_len байтов, передаваемое без изменений обработчику низкоуровневого протокола.

Параметры уровня сокета определены в заголовочном файле sys/socket.h и включают приведенные в табл. 15.4 значения.

Таблица 15.5

Параметр Описание
SO_DEBUG Включает отладочную информацию
SO_KEEPALIVE Сохраняет активными соединения при периодических передачах
SO_LINGER Завершает передачу перед закрытием

Параметры SO_DEBUG и SO_KEEPALIVE принимают целое значение option_value для установки или включения (1) и сброса или выключения (0). Для параметра SO_LINGER нужна структура типа linger, определенная в файле sys/socket.h и задающая состояние параметра и величину интервала задержки.

Функция setsockopt возвращает 0 в случае успеха и -1 в противном случае. На страницах интерактивного справочного руководства описаны дополнительные параметры и ошибки.

Множественные клиенты

До сих пор в этой главе вы видели, как применяются сокеты для реализации клиент-серверных систем, как локальных, так действующих, в сети. После установки соединения на базе сокетов они ведут себя как низкоуровневые открытые файловые дескрипторы и во многом как двунаправленные каналы.

Теперь необходимо рассмотреть случай множественных клиентов, одновременно подключающихся к серверу. Вы видели, что, когда серверная программа принимает от клиента запрос на соединение, создается новый сокет, а исходный сокет, ожидающий запросы на соединение, остается доступен для последующих запросов. Если сервер не сможет немедленно принять поступившие позже запросы на соединения, они сохранятся в очереди ожидания.

Тот факт, что исходный сокет все еще доступен, и что сокеты ведут себя как файловые дескрипторы, дает нам метод одновременного обслуживания многих клиентов. Если сервер вызовет функцию fork для создания своей второй копии, открытый сокет будет унаследован новым дочерним процессом. Далее он сможет обмениваться данными с подключившимся клиентом, в то время как основной сервер продолжит прием последующих запросов на соединение. В действительности в вашу программу сервера нужно внести очень простое изменение, показанное в упражнении 15.7.

Поскольку вы создаете дочерние процессы, но не ждете их завершения, следует сделать так, чтобы сервер игнорировал сигналы SIGCHLD, препятствуя возникновению процессов-зомби.

Упражнение 15.7. Сервер для многочисленных клиентов

1. Программа server4.c начинается так же, как последний рассмотренный сервер с важным добавлением директивы include для заголовочного файла signal.h. Переменные и процедуры создания и именования сокета остались прежними: 

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <netinet/in.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int server_sockfd, client_sockfd;

 int server_len, client_len;

 struct sockaddr_in server_address;

 struct sockaddr_in client_address;

 server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

 server_address.sin_family = AF_INET;

 server_address.sin_addr.s_addr = htonl(INADDR_ANY);

 server_address.sin_port = htons(9734);

 server_len = sizeof(server_address);

 bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

2. Создайте очередь соединений, игнорируйте подробности завершения дочернего процесса и ждите запросов клиентов:

 listen(server_sockfd, 5);

 signal(SIGCHLD, SIG_IGN);

 while(1) {

  char ch;

  printf("server waiting\n");

3. Примите запрос на соединение:

  client_len = sizeof(client_address);

  client_sockfd = accept(server_sockfd,

   (struct_sockaddr*)&client_address, &client_len);

4. Вызовите fork с целью создания процесса для данного клиента и выполните проверку, чтобы определить, родитель вы или потомок:

  if (fork() == 0) {

5. Если вы потомок, то можете читать/писать в программе-клиенте на сокете client_sockfd. Пятисекундная задержка нужна для того, чтобы это продемонстрировать:

   read(client_sockfd, &ch, 1);

   sleep(5);

   ch++;

   write(client_sockfd, &ch, 1);

   close(client_sockfd);

   exit(0);

  }

6. В противном случае вы должны быть родителем и ваша работа с данным клиентом закончена:

  else {

   close(client_socket);

  }

 }

}

Код включает пятисекундную задержку при обработке запроса клиента для имитации вычислений сервера или обращения к базе данных. Если бы вы проделали это в предыдущем сервере, каждое выполнение программы client3 заняло бы пять секунд. С новым сервером вы сможете обрабатывать множественные клиентские программы client3 параллельно с общим затраченным временем, чуть превышающим пять секунд.

$ ./server4 &

[1] 26566 server waiting

$ ./client3 & ./client3 & ./client3 & ps x

[2] 26581

[3] 26582

[4] 26583

server waiting

server waiting

server waiting

PID   TTY   STAT TIME COMMAND

26566 pts/1 S    0:00 ./server4

26581 pts/1 S    0:00 ./client3

26582 pts/1 S    0:00 ./client3

26583 pts/1 S    0:00 ./client3

26584 pts/1 R+   0:00 ps x

26585 pts/1 S    0:00 ./server4

26586 pts/1 S    0:00 ./server4

26587 pts/1 S    0:00 ./server4

$ char from server = В

char from server = В

char from server = В

ps x

PID  TTY    STAT TIME COMMAND

26566 pts/1 S    0:00 ./server4

26590 pts/1 R+   0:00 ps x

[2] Done   ./client3

[3]- Done  ./client3

[4]+ Done  ./client3

$

Как это работает

Теперь серверная программа создает новый дочерний процесс для обработки каждого клиента, поэтому вы можете видеть несколько сообщений об ожидании сервера, поскольку основная программа продолжает ждать новые запросы на подключения. В выводе команды ps (отредактированном) показан главный процесс server4 с PID, равным 26 566, который ожидает новых клиентов, в то время, как три клиентских процесса client3 обслуживаются тремя потомками сервера. После пятисекундной паузы все клиенты получают свои результаты и завершаются. Дочерние серверные процессы тоже завершаются, оставляя только один главный серверный процесс.

Серверная программа применяет вызов fork для обработки множественных клиентов. В приложении для работы с базой данных это может быть не самым удачным решением, т.к. серверная программа может быть довольно большой, и, кроме того, существует проблема координации обращений к базе данных множественных копий сервера. На самом деле, все, что вам нужно, — это способ обработки множественных клиентов единственным сервером без блокировки и ожидания доставки клиентских запросов. Решение этой задачи включает одновременную обработку множественных открытых файловых дескрипторов и не ограничено только приложениями с применением сокетов. Рассмотрим функцию select.

select

Очень часто при разработке приложений Linux вам может понадобиться проверка состояния ряда вводов для того, чтобы определить следующее предпринимаемое действие. Например, программа обмена данными, такая как эмулятор терминала, нуждается в эффективном способе одновременного чтения с клавиатуры и с последовательного порта. В однопользовательской системе подойдет цикл "активного ожидания", многократно просматривающий ввод в поиске данных и читающий их, как только они появятся. Такое поведение очень расточительно в отношении времени ЦП.

Системный вызов select позволяет программе ждать прибытия данных (или завершения вывода) одновременно на нескольких низкоуровневых файловых дескрипторах. Это означает, что программа эмулятора терминала может блокироваться до тех пор, пока у нее не появится работа. Аналогичным образом сервер может иметь дело с многочисленными клиентами, ожидая запросы одновременно на многих открытых сокетах.

Функция select оперирует структурами данных fd_set, представляющими собой множества открытых файловых дескрипторов. Для обработки этих множеств определен набор макросов:

#include <sys/types.h> #include <sys/time.h>

void FD_ZERO(fd_set *fdset);

void FD_CLR(int fd, fd_set *fdset);

void FD_SET(int fd, fd_set *fdset);

int FD_ISSET(int fd, fd_set *fdset);

Как и предполагается в соответствии с их именами, макрос FD_ZERO инициализирует структуру fd_set пустым множеством, FD_SET и FD_CLR задают и очищают элементы множества, соответствующего файловому дескриптору, переданному как параметр fd, а макрос FD_ISSET возвращает ненулевое значение, если файловый дескриптор, на который ссылается fd, является элементом структуры fd_set, на которую указывает параметр fdset. Максимальное количество файловых дескрипторов в структуре типа fd_set задается константой FD_SETDIZE.

Функция select может также использовать значение для времени ожидания, чтобы помешать бесконечной блокировке. Это значение задается с помощью структуры struct timeval. Она определена в файле sys/time.h и содержит следующие элементы:

struct timeval {

 time_t tv_sec; /* Секунды */

 long tv_usec;  /* Микросекунды */

}

Тип time_t, определенный в файле sys/types.h, — целочисленный. Системный вызов select объявляется следующим образом:

#include <sys/types.h>

#include <sys/time.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,

 fd_set *errorfds, struct timeval *timeout);

Вызов select позволяет проверить, не готов ли хотя бы один из множества файловых дескрипторов к чтению или записи, или находится ли в ожидании из-за состояния ошибки и может быть заблокирован до момента готовности одного из дескрипторов.

Аргумент nfds задает количество проверяемых файловых дескрипторов, имеются в виду дескрипторы от 0 до nfds-1. Каждое из трех множеств дескрипторов может оказаться пустым указателем, тогда связанный с ним тест не выполняется.

Функция select вернет управление, если какой-либо из дескрипторов в множестве readfds готов к чтению, какой-нибудь дескриптор из множества writefds готов к записи или у одного из дескрипторов множества errorfd есть состояние ошибки. Если ни одно из условий не соблюдается, select вернет управление после промежутка времени, заданного timeout. Если параметр timeout — пустой указатель и нет активности на сокетах, вызов может быть заблокирован на неопределенное время.

Когда select возвращает управление программе, множества дескрипторов будут модифицированы для того, чтобы указать на готовые к чтению или записи или имеющие ошибки дескрипторы. Для их проверки следует использовать макрос FD_ISSET, позволяющий определить, какие дескрипторы требуют внимания. Можно изменить значение timeout для того, чтобы показать время, остающееся до следующего превышения времени ожидания, но такое поведение не задано стандартом X/Open. При превышении времени ожидания все множества дескрипторов будут очищены.

Вызов select возвращает общее количество дескрипторов в модифицированных множествах. В случае сбоя он вернет -1 и установит значение переменной errno, описывающее ошибку. Возможные ошибки — EBADF для неверных дескрипторов, EINTR для возврата из-за прерывания и EINVAL для некорректных значений параметров nfds или timeout.

Примечание

Несмотря на то, что Linux модифицирует структуру, на которую указывает timeout, фиксируя оставшееся неиспользованное время, большинство версий UNIX этого не делают. Большая часть существующего программного кода, применяющего функцию select, инициализирует структуру типа timeval и затем продолжает использовать ее без обновления содержимого. В системе Linux этот код может выполняться некорректно, поскольку ОС Linux изменяет структуру timeval при каждом истечении отведенного времени ожидания. Если вы пишете или переносите программный код, использующий функцию select, следует учитывать эту разницу и всегда повторно инициализировать время ожидания. Имейте в виду, что оба подхода корректны, они просто разные!

Выполните упражнение 15.8.

Упражнение 15.8. Функция select

Далее для демонстрации применения функции select приведена программа select.c. Более сложный пример вы увидите чуть позже. Программа читает данные с клавиатуры (стандартный ввод — дескриптор 0) со временем ожидания 2,5 секунды. Данные читаются только тогда, когда ввод готов. Естественно расширить программу, включив в зависимости от характера приложения другие дескрипторы, такие как последовательные каналы (serial lines) и сокеты.

1. Начните как обычно с директив include и объявлений, а затем инициализируйте inputs для обработки ввода с клавиатуры:

#include <sys/types.h>

#include <sys/time.h>

#include <stdio.h>

#include <fcntl.h>

#include <sys/ioctl.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 char buffer[128];

 int result, nread;

 fd_set inputs, testfds;

 struct timeval timeout;

 FD_ZERO(&inputs);

 FD_SET(0, &inputs);

2. Подождите ввод из файла stdin в течение максимум 2,5 секунд:

 while(1) {

  testfds = inputs;

  timeout.tv_sec = 2;

  timeout.tv_usec = 500000;

  result = select(FD_SETSIZE, &testfds, (fd_set *)NULL,

   (fd_set*)NULL, &timeout);

3. Спустя это время проверьте result. Если ввода не было, программа выполнит цикл еще раз. Если в нем возникла ошибка, программа завершается:

  switch(result) {

  case 0:

   printf("timeout\n");

   break;

  case -1:

   perror("select");

   exit(1);

4. Если во время ожидания у вас наблюдаются некоторые действия, связанные с файловым дескриптором, читайте ввод из stdin и выводите его при каждом получении символа EOL (конец строки), до нажатой комбинации клавиш <Ctrl>+<D>:

  default:

   if (FD_ISSET(0, &testfds)) {

    ioctl(0, FIONREAD, &nread);

    if (nread == 0) {

     printf("keyboard done\n");

     exit(0);

    }

    nread = read(0, buffer, nread);

    buffer[nread] = 0;

    printf("read %d from keyboard: %s", nread, buffer);

   }

   break;

  }

 }

}

Во время выполнения эта программа каждые две с половиной секунды выводит строку timeout. Если вы набираете данные на клавиатуре, она читает файл стандартного ввода и отображает то, что было набрано. В большинстве командных оболочек ввод направляется в программу при нажатии пользователем клавиши <Enter> (или <Return>) или клавиш управляющей последовательности, поэтому программа будет отображать ввод каждый раз, когда вы нажимаете клавишу <Enter>. Учтите, что сама клавиша <Enter> тоже читается и обрабатывается как любой другой символ (попробуйте выполнить ввод без нажатия клавиши, введя ряд символов, за которыми следует комбинация <Ctrl>+<D>).

$ ./select

timeout

hello

read 6 from keyboard: hello

fred

read 5 from keyboard: fred

timeout

^D

keyboard done

$

Как это работает

Программа применяет вызов select для проверки состояния стандартного ввода. За счет корректировки значения времени ожидания программа каждые 2,5 секунды выводит сообщение об истечении времени ожидания. О нем свидетельствует возвращение 0 функцией select. При достижении конца файла дескриптор стандартного ввода помечается флагом как готовый к вводу, но при этом нет символов, предназначенных для считывания.

Множественные клиенты

Ваша простая серверная программа может выиграть от применения select для одновременной обработки множественных клиентов, не прибегая к помощи дочерних процессов. Используя этот метод в реальных приложениях, вы должны следить за тем, чтобы другие клиенты не ждали слишком долго, пока вы обрабатываете первого подключившегося клиента.

Сервер может применять функцию select одновременно к сокету, ожидающему запросы на подключение, и к сокетам клиентских соединений. Как только активность зафиксирована, можно использовать макрос FD_ISSET для проверки в цикле всех возможных файловых дескрипторов и выявления активных среди них.

Если сокет, ожидающий запросов на подключение, готов к вводу, это означает, что клиент пытается подсоединиться, и вы можете вызывать функцию accept без риска блокировки. Если клиентский дескриптор указывает на готовность, это означает, что есть запрос клиента, ждущий, что вы сможете прочесть и обработать его. Чтение 0 байтов означает, что клиентский процесс завершился, и вы можете закрыть сокет и удалить его из множества своих дескрипторов.

Выполните упражнение 15.9.

Упражнение 15.9. Улучшенное клиент-серверное приложение

1. В финальный пример программы server5.с вы включите заголовочные файлы sys/time.h и sys/ioctl.h вместо signal.h, использованного в предыдущей программе, и объявите несколько дополнительных переменных для работы с вызовом select:

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <netinet/in.h>

#include <sys/time.h>

#include <sys/ioctl.h>

#include <unistd.h>

#include <stdlib.h>

int main() {

 int server_sockfd, client_sockfd;

 int server_len, client_len;

 struct sockaddr_in server_address;

 struct sockaddr_in client_address;

 int result;

 fd_set readfds, testfds;

2. Создайте сокет для сервера и присвойте ему имя:

 server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

 server_address.sin_family = AF_INET;

 server_address.sin_addr.s_addr = htonl(INADDR_ANY);

 server_address.sin_port = htons(9734);

 server_len = sizeof(server_address);

 bind(serversockfd, (struct sockaddr *)&server_address, server_len);

3. Создайте очередь запросов на соединение и инициализируйте множество readfds для обработки ввода с сокета server_sockfd:

 listen(server_sockfd, 5);

 FD_ZERO(&readfds);

 FD_SET(server_sockfd, &readfds);

4. Теперь ждите запросы от клиентов. Поскольку вы передали пустой указатель как параметр timeout, не будет наступать истечения времени ожидания. Программа завершится и сообщит об ошибке, если select возвращает значение, меньшее 1.

 while(1) {

  char ch;

  int fd;

  int nread;

  testfds = readfds;

  printf("server waiting\n");

  result = select(FD_SETSIZE, &testfds, (fd_set *)0,

   (fd_set *)0, (struct timeval *)0);

  if (result < 1) {

   perror("server5");

   exit(1);

  }

5. После того как вы определили, что есть активность, можно выяснить, какой из дескрипторов активен, проверяя каждый из них по очереди с помощью макроса FD_ISSET:

  for (fd = 0; fd < FD_SETSIZE; fd++) {

   if (FD_ISSET(fd, &testfds)) {

6. Если зафиксирована активность на server_sockfd, это может быть запрос на новое соединение, и вы добавляете в множество дескрипторов соответствующий client_sockfd:

    if (fd == server_sockfd) {

     client_len = sizeof(client_address);

     client_sockfd = accept(server_sockfd,

      (struct sockaddr*)&client_address, &client_len);

     FD_SET(client_sockfd, &readfds);

     printf("adding client on fd %d\n", client_sockfd);

    }

Если активен не сервер, значит, активность проявляет клиент. Если получен close, клиент исчезает, и можно удалить его из множества дескрипторов. В противном случае вы "обслуживаете" клиента, как и в предыдущих примерах.

    else {

     ioctl(fd, FIONREAD, &nread);

     if (nread == 0) {

      close(fd);

      FD_CLR(fd, &readfds);

      printf("removing client on fd %d\n", fd);

     } else {

      read(fd, &ch, 1);

      sleep(5);

      printf("serving client on fd %d\n", fd);

      ch++;

      write(fd, &ch, 1);

     }

    }

   }

  }

 }

}

Примечание

В реальную программу было бы неплохо вставить переменную, содержащую наибольший подключенный номер fd (необязательно самый последний подключенный номер fd). Это помешает просмотру в цикле тысяч номеров fd, которые даже не подсоединены и потенциально не могут быть готовы к чтению. Мы пропустили этот фрагмент кода для краткости и простоты примера.

При запуске этой версии сервера многочисленные клиенты будут обрабатываться последовательно в единственном процессе.

$ ./server5 &

[1] 26686

server waiting

$ ./client3 & ./client3 & ./client3 & ps x

[2] 26689

[3] 26690

adding client on fd 4

server waiting

[4] 26691

PID   TTY  STAT TIME COMMAND

26686 pts/1 S   0:00 ./server5

26689 pts/1 S   0:00 ./client3

26690 pts/1 S   0:00 ./client3

26691 pts/1 S   0:00 ./client3

26692 pts/1 R+  0:00 ps x

$ serving client on fd 4

server waiting

adding client on fd 5

server waiting

adding client on fd 6

char from server = В

serving client on fd 5

server waiting

removing client on fd 4

char from server = В

serving client on fd 6

server waiting

removing client on fd 5

server waiting

char from server = В

removing client on fd 6

server waiting

[2]  Done  ./client3

[3]- Done  ./client3

[4]+ Done  ./client3

Для полноты аналогии, упомянутой в начале главы, в табл. 15.5 приведены параллели между соединениями на базе сокетов и телефонными переговорами.

Таблица 15.5

Телефон Сетевые сокеты
Звонок в компанию по номеру 555-0828 Подключение к IP-адресу 127.0.0.1
Ответ на звонок секретаря приемной Установка соединения с remote host
Просьба соединить с финансовым отделом. Маршрутизация с помощью заданного порта (9734)
Ответ на звонок администратора финансового отдела Вызов select вернул управление серверу
Звонок переадресован свободному менеджеру по работе с корпоративными заказчиками Сервер вызывает accept, создавая новый сокет на добавочный номер 456

Дейтаграммы

В этой главе мы сосредоточились на программировании приложений, поддерживающих связь со своими клиентами с помощью TCP-соединений на базе сокетов. Существуют ситуации, в которых затраты на установку и поддержку соединения с помощью сокетов излишни.

Хорошим примером может служить сервис daytime, использованный ранее в программе getdate.c. Вы создаете сокет, выполняете соединение, читаете единственный ответ и разрываете соединение. Столько операций для простого получения даты!

Сервис daytime так же доступен с помощью UDP-соединений, применяющих дейтаграммы. Для того чтобы воспользоваться им, просто пошлите сервису одну дейтаграмму и получите в ответ единственную дейтаграмму, содержащую дату и время. Все просто.

Сервисы, предоставляемые по UDP-протоколу, применяются в тех случаях, когда клиенту нужно создать короткий запрос к серверу, и он ожидает единственный короткий ответ. Если стоимость времени процессора достаточно низкая, сервер способен обеспечить такой сервис, обрабатывая запросы клиентов по одному и разрешая операционной системе хранить очередь входящих запросов. Такой подход упрощает программирование сервера.

Поскольку UDP — не дающий гарантий сервис, вы можете столкнуться с потерей вашей дейтаграммы или ответа сервера. Если данные важны для вас, возможно, придется тщательно программировать ваших UDP-клиентов, проверяя ошибки и при необходимости повторяя попытки. На практике в локальных сетях UDP-дейтаграммы очень надежны.

Для доступа к сервису, обеспечиваемому UDP-протоколом, вам следует применять системные вызовы socket и close, но вместо использования вызовов read и write для сокета вы применяете два системных вызова, характерных для дейтаграмм: sendto и recvfrom.

Далее приведена модифицированная версия программы getdate.c, которая получает дату с помощью сервиса UDP-дейтаграмм. Изменения по сравнению с предыдущей версией выделены цветом.

/* Начните с обычных include и объявлений. */

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main(int argc, char *argv[]) {

 char *host;

 int sockfd;

 int len, result;

 struct sockaddr_in address;

 struct hostent *hostinfo;

 struct servent *servinfo;

 char buffer[128];

 if (argc == 1) host = "localhost";

 else host = argv[1];

 /* Ищет адрес хоста и сообщает об ошибке, если не находит. */

 hostinfo = gethostbyname(host);

 if (!hostinfo) {

  fprintf(stderr, "no host: %s\n", host);

  exit(1);

 }

 /* Проверяет наличие на компьютере сервиса daytime. */

 servinfo = getservbyname("daytime", "udp");

 if (!servinfo) {

  fprintf(stderr, "no daytime service\n");

  exit(1);

 }

 printf("daytime port is %d\n", ntohs(servinfo->s_port));

 /* Создает UDP-сокет. */

 sockfd = socket(AF_INEТ, SOCK_DGRAM, 0); 

 /* Формирует адрес для использования в вызовах sendto/recvfrom... */

 address.sin_family = AF_INET;

 address.sin_port = servinfo->s_port;

 address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;

 len = sizeof(address);

 result = sendto(sockfd, buffer, 1, 0, (struct sockaddr *)&address, len);

 result = recvfrom(sockfd, buffer, sizeof(buffer), 0,

  (struct sockaddr *)&address, &len);

 buffer [result] = '\0';

 printf("read %d bytes: %s", result, buffer);

 close(sockfd);

 exit(0);

}

Как видите, необходимы лишь незначительные изменения. Как и раньше, вы ищете сервис daytime с помощью вызова getservbyname, но задаете дейтаграммный сервис, запрашивая UDP-протокол. Дейтаграммный сокет создается с помощью вызова socket с параметром SOCK_DGRAM. Адрес назначения задается, как и раньше, но теперь вместо чтения из сокета вы должны послать дейтаграмму.

Поскольку вы не устанавливаете явное соединение с сервисами на базе UDP, у вас должен быть способ оповещения сервера о том, что вы хотите получить ответ. В данном случае вы посылаете дейтаграмму (в нашем примере вы отправляете один байт из буфера, в который вы хотите получить ответ) сервису и он посылает в ответ дату и время.

Системный вызов sendto отправляет дейтаграмму из буфера на сокет, используя адрес сокета и длину адреса. У этого вызова фактически следующий прототип:

int sendto(int sockfd, void *buffer, size_t len, int flags,

 struct sockaddr *to, socklen_t tolen);

В случае обычного применения параметр flags можно оставлять нулевым.

Системный вызов recvfrom ожидает дейтаграмму в соединении сокета с заданным адресом и помещает ее в буфер. У этого вызова следующий прототип:

int recvfrom(int sockfd, void *buffer, size_t len, int flags,

 struct sockaddr *from, socklen_t *fromlen);

И снова в случае обычного применения параметр flags можно оставлять нулевым.

Для упрощения примера мы пропустили обработку ошибок. Оба вызова, sendto и recvfrom, в случае возникновения ошибки вернут -1 и присвоят переменной errno соответствующее значение. Возможные ошибки перечислены в табл. 15.6.

Таблица 15.6

Значение errno Описание
EBADF Был передан неверный файловый дескриптор
EINTR Появился сигнал

Если сокет не был определен как неблокирующийся с помощью вызова fcntl (как вы видели ранее для TCP-соединений), вызов recvfrom будет заблокирован на неопределенное время. Но сокет можно использовать с помощью вызова select и времени ожидания, позволяющих определить, поступили ли данные, так же, как в случае серверов с устанавливаемыми соединениями. В противном случае можно применить сигнал тревоги для прерывания операции получения данных (см. главу 11).

Резюме 

В этой главе мы предложили еще один способ взаимодействия процессов — сокеты. Они позволяют разрабатывать по-настоящему распределенные клиент-серверные приложения, которые выполняются в сетевой среде. Было дано краткое описание некоторых информационных функций базы данных сетевых узлов и способы обработки в системе Linux стандартных системных сервисов с помощью интернет-демонов. Вы проработали ряд примеров клиент-серверных программ, демонстрирующих обработку и сетевую организацию множественных клиентов.

В заключение вы познакомились с системным вызовом select, позволяющим уведомлять программу об активности ввода и вывода сразу на нескольких открытых файловых дескрипторах и сокетах. 

Глава 16

Программирование в GNOME с помощью GTK+

До сих пор в этой книге мы обсуждали основные методы программирования в ОС Linux, касающиеся сложной внутренней начинки. Теперь же пора вдохнуть жизнь в наши приложения и узнать, как включить в них графический пользовательский интерфейс (Graphical User Interface, GUI). В этой главе и в главе 17 мы собираемся рассмотреть две самые популярные библиотеки GUI для ОС Linux: GTK+ и KDE/Qt. Эти библиотеки соответствуют двум популярнейшим интегрированным средам рабочего стола Linux: GNOME (GTK+) и KDE.

Все библиотеки GUI в Linux размещены поверх низкоуровневой оконной системы, называемой X Window System (чаще X11 или просто X), поэтому, прежде чем вдаваться в подробности среды GNOME/GTK+, мы приведем обзор основных принципов работы системы X и поможем понять, как различные слои оконной системы пригоняются один к другому для создания того, что мы называем рабочим столом.

В этой главе обсуждаются следующие темы:

□ система X Window System;

□ введение в среду GNOME/GTK+;

□ виджеты или интерфейсные элементы окна GTK+;

□ виджеты и меню среды GNOME;

□ диалоговые окна;

□ GUI базы данных компакт-дисков с использованием GNOME/GTK+.

Введение в систему X

Если вы когда-либо применяли оконную систему рабочего стола в ОС Linux, скорее всего вы использовали графическую систему X с открытым программным кодом. Одна из наиболее передовых и в результате разочаровывающих характеристик X — жесткая привязка к идеологии "инструментов, а не политики". Это означает, что в системе X нет определения какого-либо пользовательского интерфейса, но есть средства для его создания. Вы вольны создавать целиком собственную среду рабочего стола, экспериментируя и вводя новшества при желании. Но это же свойство долгое время тормозило разработку пользовательских интерфейсов в системах Linux и UNIX. Для заполнения этой пустоты возникли два проекта рабочего стола, предпочитаемые пользователями Linux: GNOME и KDE. Рабочий стол ОС Linux, тем не менее, не ограничивается системой X. В действительности рабочий стол в Linux — это довольно расплывчатая субстанция без определенной версии, выпущенной в рамках одного проекта или какой-либо группой специалистов. Современная установка содержит мириады библиотек, утилит и приложений, которые все вместе называются "рабочим столом".

У системы X, первоначально разработанной в MIT (Массачусетский технологический институт) в начале 1980 гг., длинная и яркая история. Она создавалась как унифицированная оконная система для высокопроизводительных рабочих станций того времени, которые были очень дорогими перемалывающими огромные объемы чисел чудовищами.

Когда наступили 1990 гг. и цены на оборудование упали, энтузиасты перенесли систему X на недорогие домашние компьютеры PC, этот проект стал называться XFree86 (процессоры PC, выпускавшиеся корпорацией Intel и другими компаниями, были известны как процессоры x86), и сегодня вместе с системой Linux распространяются потомки проекта XFree86, а в большинстве дистрибутивов Linux применяется вариант системы X, названный X.Org,

X Window System разделена на компоненты аппаратного и программного уровней, называемые Х-сервером и Х-клиентом. Эти компоненты взаимодействуют с помощью протокола с легко угадываемым названием "X-протокол". В следующих разделах рассматривается по очереди каждый из этих компонентов.

X-сервер

X-сервер запускается на пользовательской локальной машине и выполняет низкоуровневые операции прорисовки графического экрана. Присутствие в названии слова "сервер" часто смущает: X-сервер выполняется на вашем настольном ПК. X-клиенты могут запускаться на вашем настольном ПК или на самом деле выполняться на других компьютерах в вашей сети, включая серверы. Если подумать, обратная терминология не лишена смысла, но часто кажется применяемой задом наперед.

Поскольку X-сервер напрямую общается с видеокартой, вы должны применять X-сервер, соответствующий вашей видеокарте, и для него следует задавать подходящее разрешение, скорость обновления экрана, количество цветов и т.д. Файл конфигурации называется xorg.conf или Xfree86Config. В прошлом вы обычно должны были вручную редактировать файл конфигурации, чтобы добиться корректной работы системы X. К счастью, современные дистрибутивы Linux автоматически определяют нужные установочные параметры, экономя время пользователя и избавляя его от решения головоломок!

X-сервер ждет ввод пользователя от мыши и клавиатуры и передает нажатия клавиш и щелчки кнопками мыши приложениям, X-клиентам. Эти сообщения называют событиями; они служат основными элементами программирования GUI. Позже в этой главе мы подробно рассмотрим события и их логическое расширение GTK+ сигналы.

X-клиент

X-клиент — это любая программа, использующая X Window System как GUI. Примерами могут служить xterm, xcalc и более сложные, приложения, например, Abiword. X-клиент ждет события пользователя, посылаемые X-сервером, и отвечает на них отправкой обратно серверу сообщений об обновлении изображений.

Примечание

X-клиент необязательно должен быть на той же машине, что и X-сервер.

X-протокол

X-клиент и X-сервер взаимодействуют с помощью X-протокола, который позволяет клиенту и серверу быть разделенными сетью. Например, вы можете запустить приложение X-клиент с удаленного компьютера через Интернет или шифруемую виртуальную частную сеть (Virtual Private Network, VPN). В большинстве персональных систем Linux X-клиенты и X-сервер работают в одной и той же системе.

Xlib

Xlib — это библиотека, неявно используемая X-клиентом для генерации сообщений X-протокола. Она предоставляет API очень низкого уровня, позволяющий клиенту отображать простейшие элементы на X-сервере и откликаться на простейший ввод. Мы должны подчеркнуть, что Xlib — это библиотека очень низкого уровня, и создание с ее применением даже чего-либо столь же простого, как меню, — невероятно трудоемкий процесс, требующий сотен строк программного кода.

Разработчик GUI не может эффективно программировать непосредственно с помощью Xlib. Вам нужен API, делающий легким и простым создание таких элементов GUI, как меню, кнопки и раскрывающиеся списки. Говоря кратко, эту роль играет комплект инструментальных средств или элементов интерфейса.

Комплекты инструментов

Комплект инструментов или элементов интерфейса — это библиотека GUI, которую X-клиенты применяют для значительного упрощения создания окон, меню, кнопок и т.п. С помощью комплекта инструментальных средств вы можете создавать кнопки, меню, фреймы и тому подобное с помощью вызовов одной функции. Общий термин для обозначения элементов GUI, подобных перечисленным, — виджеты, универсальный элемент, который вы найдете во всех современных библиотеках GUI.

Существует масса комплектов инструментов для системы X, из которых вы можете выбирать, и каждый из них обладает определенными достоинствами и недостатками. На каком остановиться — важное проектное решение для вашего приложения, и при выборе следует учитывать следующие факторы.

□ Для кого предназначено ваше приложение?

□ Будут ли установлены библиотеки комплекта инструментов у ваших пользователей?

□ Перенесен ли комплект инструментов в другие популярные операционные системы?

□ Какой лицензией программного обеспечения пользуется комплект инструментов и согласуется ли она с предполагаемым вами использованием?

□ Поддерживает ли комплект инструментов ваш язык программирования?

□ Современный ли внешний вид и реализация у комплекта инструментов?

На протяжении многих лет самыми популярными комплектами инструментальных средств были Motif, OpenLook и Xt, но они уступили технически более совершенным комплектам GTK+ и Qt, формирующим основу рабочих столов GNOME и KDE соответственно.

Оконные менеджеры

Последний компонент X-мозаики — оконный менеджер или диспетчер, который отвечает за расположение окон на экране. Оконные менеджеры часто поддерживают отдельные "рабочие области", на которые делится рабочий стол, увеличивая область экрана, с которой вы можете взаимодействовать. Оконный менеджер также отвечает за графическое оформление всех окон, состоящее, как правило, из рамки и полосы заголовка с пиктограммами максимизации, минимизации и закрытия окна. Оконные менеджеры обеспечивают частично внешний вид рабочего стола, например заголовки окон.

К широко распространенным относятся следующие оконные менеджеры:

□ Metacity — оконный менеджер, используемый по умолчанию для рабочего стола GNOME;

□ KWin — оконный менеджер, применяемый по умолчанию для рабочего стола KDE;

□ Openbox — разработанный для экономии ресурсов и запускаемый на более старых и медленных системах;

□ Enlightenment — оконный менеджер, отображающий превосходную графику и спецэффекты.

Как и все в системе X, оконные менеджеры можно переключать. Тем не менее, большинство пользователей запускает оконный менеджер, входящий в их поставку среды рабочего стола.

Другие способы создания GUI — платформно-независимые оконные API

Следует упомянуть и другие способы создания GUI, характерные для ОС Linux, — существуют языки с собственной поддержкой GUI, функционирующей под управлением Linux.

□ Язык Java поддерживает программирование GUI с помощью Swing и более старых API AWT. Внешнее оформление GUI на языке Java понравится не всем и на более старых машинах интерфейс может восприниматься как громоздкий и медленно реагирующий. Огромное преимущество Java заключается в том, что единожды откомпилированный код на языке Java выполняется неизменным да любой платформе с виртуальной машиной Java (Java Virtual Machine), которая включает Linux, Windows, Mac OS и мобильные устройства. Дополнительную информацию см. на Web-сайте http://java.sun.com.

□ Язык программирования C# очень похож на язык Java. В систему Linux общеязыковая исполняющая среда (C# Common Language или CLR) пришла из проекта Mono, см. Web-сайт http://www.mono-project.com. C# на платформе Mono поддерживает модель программирования Windows Forms, применяемую в Windows, и специальную привязку к комплекту инструментов GTK+, названную Gtk#.

□ Tcl/Tk — язык сценариев, отлично подходящий для быстрой разработки интерфейсов GUI и работающий с X, Windows и Mac OS. Он очень удобен для быстрого макетирования или маленьких утилит, нуждающихся в простоте и удобстве сопровождения сценария. Все подробности можно найти на Web-сайте http://tcl.tk.

□ Python — тоже язык сценариев. Вы можете применять Tk, часть Tcl/Tk, из Python или программировать в привязке Python к GTK+, разрабатывая программы GTK+ на языке Python. Дополнительную информацию о языке Python см. на Web-сайте http://www.python.org.

□ Perl — еще один популярный язык сценариев в Linux. Вы можете применять Tk, часть Tcl/Tk, в языке Perl как Perl/Tk. Дополнительную информацию о Perl см. на Web-сайте http://www.perl.org/.

За платформою независимость, которую приносят эти языки, приходится платить. Совместное использование данных их собственными приложениями — например, применение операции перетаскивания мышью (drag and drop) — затруднено, и сохранение конфигурации обычно следует выполнять специфическим, а не стандартным способом, характерным для рабочего стола. Иногда поставщики программного обеспечения на языке Java хитрят, включая в поставку платформно-зависимые расширения для того, чтобы избежать подобных проблем.

Введение в GTK+

Теперь, когда вы познакомились с системой X Window System, самое время рассмотреть комплект инструментальных средств GTK+ Toolkit. GTK+ появился на свет как часть популярного графического редактора GNU Image Manipulation Program (GIMP), от которого он и унаследовал свое имя (The Gimp ToolKit). Очевидно, что программисты GIMP всерьез предвидели превращение GTK+ в самостоятельный проект, поскольку он вырос и стал одним из самых мощных и популярных комплектов инструментов. Домашнюю страницу проекта GTK+ можно найти по адресу http://www.gtk.org.

Примечание

В итоге, GTK+ — это библиотека, которая существенно упрощает создание графических интерфейсов пользователей (Graphical User Interface, GUI), предоставляя набор готовых компонентов, именуемых виджетами, которые вы соединяете вместе с помощью легких в использовании вызовов функций, включенных в логическую структуру вашего приложения.

Несмотря на то, что GTK+ — это проект GNU, как и GIMP, он выпущен на условиях более либеральной лицензии (Lesser General Public License, Стандартная общественная лицензия ограниченного применения GNU), которая освобождает программное обеспечение (включая патентованное программное обеспечение с закрытым программным кодом), написанное с использованием GTK+, от уплаты лицензионных вознаграждений или авторских гонораров, а также других ограничений. Свобода, предлагаемая лицензией GTK+, отличает этот комплект инструментов от его конкурента Qt (который будет обсуждаться в следующей главе), чья лицензия GPL запрещает разработку коммерческого программного обеспечения с использованием Qt (в этом случае вы должны купить коммерческую лицензию для Qt).

Комплект GTK+ целиком написан на языке С и большая часть программного обеспечения GTK+ также написана на С. К счастью, существует ряд привязок к языкам (language binding), позволяющих применять GTK+ в предпочитаемом вами языке программирования, будь то С++, Python, PHP, Ruby, Perl, C# или Java.

Комплект GTK+ сформирован как надстройка для ряда других библиотек, К ним относятся следующие:

□ GLib — предоставляет низкоуровневые структуры данных, типы, поддержку потоков, циклов событий и динамической загрузки;

□ GObject — реализует объектно-ориентированную систему на языке С, не требующую применения языка С++;

□ Pango — поддерживает визуализацию и форматирование текста;

□ ATK — помогает создавать приложения с доступом и позволяет пользователям запускать ваши приложения с помощью средств чтения экрана и других средств доступа;

□ GDK (GIMP Drawing Kit) — обрабатывает визуализацию низкоуровневой графики поверх библиотеки Xlib;

□ GdkPixbuf — помогает манипулировать изображениями в программах GTK+;

□ Xlib — предоставляет низкоуровневую графику в системах Linux и UNIX.

Система типов GLib

Если вы когда-нибудь просматривали программный код GTK+, то могли удивиться, увидев множество типов данных языка С с префиксом g, например, gint, gchar, gshort, а также незнакомые типы gint32 и gpointer. Дело в том, что комплект GTK+ основан на библиотеках переносимости языка С (portability libraries), названных GLib и GObject, которые определяют эти типы для того, чтобы способствовать межплатформным разработкам.

GLib и GObject помогают межплатформным разработкам, обеспечивая стандартный набор типов данных замещения, функций и макросов для поддержки управления памятью и общих задач. Эти типы, функции и макросы означают, что, как программисты GTK+, мы можем быть уверены в том, что наш программный код надежно переносится на другие платформы и архитектуры.

В библиотеке Glib также определено несколько очень удобных констант:

#include <glib/gmacros.h>

#define FALSE 0

#define TRUE !FALSE

Дополнительные типы данных — это типы, служащие заменой для стандартных типов данных C (из соображений совместимости и читабельности) и гарантирующие одинаковый размер в байтах на. всех платформах:

□ gint, guint, gchar, guchar, glong, gulong, gfloat и gdouble — просто замены для стандартных типов С для совместимости;

□ gpointer — синоним типа (void*);

□ gboolean — полезен для представления логических значений и служит оболочкой для int;

□ gint8, guint8, gint16, guint16, gint32 и guint32 — знаковые и беззнаковые типы с гарантированным размером в байтах.

Удобно то, что применение библиотек GLib и GObject почти прозрачно. Glib широко используется в GTK+, поэтому если у вас есть работающая установка GTK+, то вы обнаружите, что библиотека Glib уже установлена. Как вы увидите позже в этой главе, при программировании с помощью комплекта GTK+ вам даже не придется явно включать заголовочный файл glib.h.

Система объектов GTK+

Все, у кого уже есть опыт программирования GUI, возможно, поймут наше утверждение о строгой приверженности библиотек GUI концепции объектно-ориентированного программирования (ООП), настолько строгой, что все современные комплекты инструментов, включая GTK+, написаны в стиле объектно-ориентированного программирования.

Несмотря на то, что комплект инструментов GTK+ написан на чистом С, он поддерживает объекты и ООП благодаря библиотеке GObject. Эта библиотека поддерживает наследование объектов и полиморфизм с помощью макросов.

Давайте рассмотрим образец наследования и полиморфизма на примере иерархии объектов GtkWindow, взятой из документации GTK+ API.

GObject

 +---GInitiallyUnowned

 +----GtkObject

       +----GtkWidget

             +----GtkContainer

                   +----GtkBin

                         +----GtkWindow

Этот список объектов говорит о том, что объект GtkWindow — потомок GtkBin, и, следовательно, любую функцию, которую вы вызываете с объектом GtkBin, вы можете вызвать и с объектом GtkWindow. Точно так же объект GtkWindow наследует из объекта GtkContainer, который в свою очередь наследует из объекта GtkWidget.

Для удобства все функции создания виджетов возвращают тип GtkWidget. Например,

GtkWidget* gtk_window_new(GtkWindowType type);

Предположим, что вы создаете объект GtkWindow и хотите передать возвращенное значение в функцию, ожидающую объект типа GtkContainer, например, такую, как gtk_container_add:

void gtk_container_add(GtkContainer* container, GtkWidget *widget);

Вы применяете макрос GTK_CONTAINER для приведения типов GtkWidget и GtkContainer:

GtkWidget * window = gtk_window_new(GTK GTK_WINDOW_TOPLEVEL);

gtk_container_add(GTK_CONTAINER(window), awidget);

Назначение этих функций вы узнаете позже; сейчас просто отметьте для себя частое применение макросов. Для каждого возможного приведения типа существует макрос.

Примечание

Не беспокойтесь, если вам все это не очень понятно; вам не нужно разбираться в подробностях ООП для того, чтобы освоить GNOME/GTK+. На самом деле это безболезненный способ усвоить идеи и преимущества ООП на базе знакомого вам языка С.

Знакомство с GNOME

GNOME — имя, данное проекту, начатому в 1997 г. программистами, работавшими в проекте GNU Image Manipulation Program (GIMP) над созданием унифицированного рабочего стола для Linux. Все были согласны с тем, что выбор ОС Linux как платформы рабочего стола тормозился отсутствием согласованной стратегии. В то время рабочий стол Linux напоминал Дикий Запад без общих стандартов или выработанных на практике приемов, и программисты могли делать все, что вздумается. Без сводной группы, контролирующей меню рабочего стола, согласованное представление и отображение, документацию, трансляцию и т.д., освоение рабочего стола новичком было в лучшем случае путанным, а в худшем — непригодным.

Группа GNOME намеревалась создать рабочий стол для ОС Linux с лицензией GPL, разрабатывая утилиты и программы настройки в едином согласованном стиле, одновременно способствуя развитию стандартов для взаимодействия приложений, печати, управления сеансами и лучших приемов в программировании GUI приложений.

Результаты их стараний очевидны: среда GNOME — основа стандартного рабочего стола Linux в дистрибутивах Fedora, Red Hat, Ubuntu, openSUSE и др. (рис. 16.1).

Первоначально название GNOME означало GNU Network Object Model Environment (среда сетевых объектных моделей GNU), что отражает одну из ранее поставленных задач, внедрение в систему Linux объектной интегрированной системы, такой как Microsoft OLE, для того, чтобы вы могли, например, встроить электронную таблицу в документ текстового процессора. Теперь поставлены новые задачи, и то, что сегодня нам известно как GNOME, — это законченная среда рабочего стола, содержащая панель для запуска приложений, комплект программ и утилит, библиотеки программирования и средства поддержки разработчиков.

Перед тем как начать программировать, следует убедиться в том, что все библиотеки установлены.

Рис. 16.1 

Установка библиотек разработки GNOME/GTK+

Полный рабочий стол GNOME со своими стандартными приложениями и библиотеками разработки GNOME/GTK+ включает в себя более 60 пакетов, поэтому установка GNOME с нуля вручную или из исходного кода — устрашающая перспектива. К счастью, в современных дистрибутивах Linux есть отличные утилиты управления пакетами, превращающие установку GNOME/GTK+ и библиотек разработки в пустяковое дело.

В дистрибутивах Linux Red Hat и Fedora вы открываете средство Package Management (Управление пакетами), щелкнув мышью кнопку меню Applications (Приложения) и выбрав команду Add/Remove Software (Добавить/удалить программы). Когда появится Package Management (рис. 16.2), убедитесь в том, что установлен флажок GNOME Software Development (Разработка программ GNOME). Загляните в область Development (Разработка) для этого установочного параметра.

В этой главе вы будете работать с GNOME/GTK+ 2, поэтому убедитесь в том, что установлены библиотеки версии 2.x.Рис. 16.2 

В случае дистрибутивов, применяющих RPM-пакеты, у вас должны быть установлены как минимум следующие RPM-пакеты:

□ gtk2-2.10.11-7.fc7.rpm;

□ gtk2-devel-2.10.11-7.fc7.rpm;

□ gtk2-engines-2.10.0-3.fc7.rpm;

□ libgnome-2.18.0-4.fc7.rpm;

□ libgnomeui-2.18.l-2.fc7.rpm;

□ libgnome-devel-2.18.0-4.fc7.rpm;

□ libgnomeui-devel-2.18.1-2.fc7.rpm.

Примечание

В этом примере комбинация символов fc7 указывает на дистрибутив Linux Fedora 7. В вашей системе могут быть слегка отличающиеся имена.

В дистрибутиве Debian и основанных на Debian системах, таких как Ubuntu, вы можете использовать программу apt-get для установки пакетов GNOME/GTK+ с разных сайтов-зеркал (mirrors). Для выяснения подробностей следуйте по ссылкам Web-сайта http://www.gnome.org.

Опробуйте также демонстрационное приложение GTK+, в котором показаны все виджеты и их оформление (рис. 16.3).

$ gtk-demo

Рис. 16.3 

Примечание

Для каждого виджета отображаются вкладки Info (Информация) и Source (Исходный код). На вкладке Source (Исходный код) приведен программный код на языке С для применения данного виджета. На ней может быть представлено множество примеров.

Выполните упражнение 16.1.

Упражнение 16.1. Обычное окно GtkWindow

Давайте начнем программирование средствами GTK+ с простейшей из программ GUI — отображения окна. Вы увидите библиотеки GTK+ в действии и большой набор функциональных возможностей, получаемых из очень короткого программного кода.

1. Введите программу и назовите ее gtk1.с:

#include <gtk/gtk.h>

int main(int argc, char *argv[]) {

 GtkWidget *window;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_widget_show(window);

 gtk_main();

 return 0;

}

2. Для компиляции gtk1.c введите следующую команду:

$ gcc gtk1.c -о gtk1 `pkg-config --cflags --libs gtk+-2.0`

Примечание

Будьте внимательны и набирайте обратные апострофы, а не обычные апострофы — помните о том, что обратные апострофы — это инструкции, заставляющие оболочку выполнить заключенную в них команду и добавить ее вывод в конец строки.

Когда вы выполните программу с помощью следующей команды, ваше окно должно раскрыться (рис. 16.4).

$ ./gtk1

Рис. 16.4 

Учтите, что вы можете перемещать окно, изменять его размер, сворачивать и раскрывать его на весь экран.

Как это работает

Включить заголовочные файлы, необходимые для библиотек GTK+ и связанных с ними библиотек, можно с помощью одного оператора #include <gtk/gtk.h>. Далее вы объявляете окно как указатель на объект GtkWidget.

Затем для инициализации библиотек GTK+ следует выполнить вызов gtk_init, передав аргументы командной строки argc и argv. Это дает возможность GTK+ выполнить синтаксический анализ любых параметров командной строки, о которых комплект должен знать. Учтите, что вы всегда должны инициализировать GTK+ таким способом перед вызовом любых функций GTK+.

Суть примера заключается в вызове функции gtk_window_new. Далее приведен ее прототип:

GtkWidget* gtk_window_new(GtkWindowType type);

Параметр type может принимать в зависимости от назначения окна одно из двух значений:

□ GTK_WINDOW_TOPLEVEL — стандартное окно с рамкой;

□ GTK_WINDOW_POPUP — окно без рамки, подходящее для диалогового окна.

Почти всегда вы будете применять значение GTK_WINDOW_TOPLEVEL, потому что для создания диалоговых окон, как вы узнаете позже, есть гораздо более удобные способы.

Вызов gtk_window_new создает окно в памяти, таким образом у вас появляется возможность перед реальным выводом окна на экран заполнить его виджетами, изменить размер окна, его заголовок и т.д. Для того чтобы окно появилось на экране, выполните вызов функции gtk_widget_show:

gtk_widget_show(window);

Эта функция принимает указатель типа GtkWidget, поэтому вы просто предоставляете ссылку на свое окно.

Последним вы выполняете вызов функции gtk_main. Эта основная функция запускает процесс обмена информацией (interactivity process), передавая управление GTK+, и не возвращает его до тех пор, пока не будет выполнен вызов функции gtk_main_quit. Как видно в программе gtk1.с, этого никогда не происходит, поэтому приложение не завершается даже после закрытия окна. Проверьте это, щелкнув кнопкой мыши пиктограмму закрытия окна и убедившись в отсутствии строки, приглашающей вводить команду. Вы исправите это поведение после того, как познакомитесь с сигналами и обратными вызовами в следующем разделе. Сейчас завершите приложение, нажав комбинацию клавиш <Ctrl>+<C> в окне командной оболочки, которое вы использовали для запуска программы gtk1.

События, сигналы и обратные вызовы

У всех библиотек GUI есть нечто общее. Должен существовать некий механизм для выполнения программного кода в ответ на действие пользователя. Программа, выполняющаяся в режиме командной строки, может позволить себе останов выполнения в ожидании ввода и затем применить нечто вроде оператора выбора для выполнения разных ветвей программы в зависимости от введенных данных. Такой подход нецелесообразен в случае приложения GUI, поскольку оно должно непрерывно реагировать на ввод пользователя, например, ему приходится постоянно обновлять области окна.

У современных оконных систем есть система событий и приемники событий, которым адресована эта задача. Идея заключается в том, что каждый пользовательский ввод обычно с помощью мыши или клавиатуры инициирует событие. Нажатие на клавиатуре, например, вызовет "событие клавиатуры". Затем пишется программный код, который ждет приема такого события и выполняется в случае его возникновения.

Как вы уже видели, эти события генерирует система X Window System, но они мало помогут вам как программисту GTK+, т.к. они очень низкоуровневые. Когда производится щелчок кнопкой мыши, X порождает событие, содержащее координаты указателя мыши, а вам нужно знать, когда пользователь активизирует виджет.

У GTK+ есть собственная система событий и приемников событий, называемых сигналами и обратными вызовами. Их очень легко применять, поскольку для установки обработчика сигнала можно использовать очень полезное свойство языка С, указатель на функцию.

Сначала несколько определений. Сигнал GTK+ порождается объектом типа GtkObject, когда происходит нечто, например, ввод пользователя. Функция, связанная с сигналом и, следовательно, вызываемая при любом порождении сигнала, называется функцией обратного вызова.

Примечание

Имейте в виду, что сигнал GTK+ — это нечто иное, чем сигнал UNIX, обсуждавшийся в главе 11.

Как программист, использующий GTK+, вы должны заботиться только о написании и связывании функций обратного вызова, поскольку код порождения сигнала — это внутренний программный код определенного виджета.

Прототип или заголовок функции обратного вызова обычно похож на следующий:

void a_callback_function(GtkWidget *widget, gpointer user_data);

Вы передаете два параметра: первый — указатель на виджет, породивший сигнал, второй — произвольный указатель, который вы выбираете самостоятельно, когда связываете обратный вызов. Вы можете использовать этот указатель для любых целей.

Связать функцию обратного вызова тоже очень просто. Вы вызываете функцию g_signal_connect и передаете ей виджет, имя сигнала в виде строки, указатель на функцию обратного вызова и ваш произвольный указатель:

gulong g_signal_connect(gpointer *object, const gchar *name,

 GCallback func, gpointer user_data);

Следует отметить, что для связывания функций обратного вызова нет ограничений. Вы можете иметь много сигналов, связанных с одной и той же функцией обратного вызова, и много функций обратного вызова, связанных с единственным сигналом.

В документации по API GTK+ можно найти подробное описание сигналов, порождаемых каждым виджетом.

Примечание

До появления GTK+ 2 для связывания функций обратного вызова применялась функция gtk_signal_connect. Она была заменена функцией g_signal_connect и не должна применяться во вновь разрабатываемом программном коде.

Вы опробуете функцию g_signal_connect в упражнении 16.2.

Упражнение 16.2. Функция обратного вызова

В программе gtk2.c вставьте в свое окно кнопку и свяжите сигнал clicked (щелчок мышью по кнопке) с вашей функцией обратного вызова для вывода короткого сообщения:

#include <gtk/gtk.h>

#include <stdio.h>

static int count = 0;

void button_clicked(GtkWidget *button, gpointer data) {

 printf("%s pressed %d time(s) \n", (char *)data, ++count);

}

int main(int argc, char* argv[]) {

 GtkWidget *window;

 GtkWidget *button;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 button = gtk_button_new_with_label("Hello World!");

 gtk_container_add(GTK_CONTAINER(window), button);

 g_signal_connect(GTK_OBJECT(button), "clicked",

  GTK_SIGNAL_FUNC(button_clicked), "Button 1");

 gtk_widget_show(button);

 gtk_widget_show(window);

 gtk_main();

 return 0;

}

Введите исходный текст программы и сохраните его в файле с именем gtk2.c. Откомпилируйте и скомпонуйте программу аналогично программе gtk1.с из предыдущего упражнения. Запустив ее, вы получите окно с кнопкой. При каждом щелчке кнопки мышью будет выводиться короткое сообщение (рис. 16.5).

Рис. 16.5

Как это работает

Вы добавили два новых элемента в программу gtk2.c: виджет GtkButton и функцию обратного вызова. GtkButton — это виджет простой кнопки, которая может содержать текст, в нашем случае "Hello World", и порождает сигнал, названный clicked, каждый раз, когда кнопку щелкают мышью.

Функция обратного вызова button_clicked связана с сигналом clicked виджета кнопки с помощью функции g_signal_connect:

g_signal_connect(GTK_OBJECT(app), "clicked",

 GTK_SIGNAL_FUNC(button_clicked), "Button 1");

Обратите внимание на то, что имя кнопки — "Button 1" — передается в функцию обратного вызова как данные пользователя.

Весь остальной добавленный программный код касается виджета кнопки, создаваемой так же, как окно — вызовом функции gtk_button_new_with_label — функция gtk_widget_show делает ее видимой.

Для расположения кнопки в окне вызывается функция gtk_container_add. Эта простая функция помещает GtkWidget внутрь объекта GtkContainer и принимает контейнер и виджет как аргументы: 

void gtk_container_add(GtkContainer* container, GtkWidget *widget); 

Как вы уже знаете, GtkWindow — потомок или дочерний объект объекта GtkContainer. поэтому вы можете привести тип вашего объекта-окна к типу GtkContainer с помощью макроса GTK_CONTAINER:

gtk_container_add(GTK_CONTAINER(window), button);

Функция gtk_container_add прекрасно подходит для расположения в окне одиночного виджета, но гораздо чаще вам потребуется для создания хорошего интерфейса размещать несколько виджетов в разных частях окна. У комплекта GTK+ есть специальные виджеты как раз для этой цели, именуемые виджетами упаковочных контейнеров,

Виджеты упаковочных контейнеров

Компоновка GUI исключительно важна для удобства применения интерфейса, и добиться наилучшей компоновки труднее всего. Реальная трудность в размещении виджетов заключается в том, что вы не можете полагаться на наличие у всех пользователей одинаковых размеров окон, тем, шрифтов и цветовых схем. То, что может быть отличным интерфейсом для одной системы, в другой системе может оказаться просто нечитаемым.

Для создания GUI, который выглядит одинаково во всех системах, вам необходимо избегать размещения виджетов на основе абсолютных координат и использовать более гибкую систему компоновки. У GTK+ есть для этой цели виджеты контейнеров. Виджеты-контейнеры позволяют управлять компоновкой виджетов в окнах вашего приложения. Виджеты упаковочных контейнеров (box) представляют очень удобный тип виджета-контейнера. GTK+ предлагает множество виджетов-контейнеров других типов, описанных в интерактивной документации к GTK+.

Виджеты упаковочных контейнеров — невидимые виджеты, задача которых — хранить другие виджеты и управлять их компоновкой или схемой размещения. Для управления размером отдельных виджетов, содержащихся в виджете упаковочного контейнера, вы задаете правила вместо координат. Поскольку виджеты упаковочных контейнеров могут содержать любые объекты GtkWidget и объект GtkBox сам является объектом типа GtkWidget, для создания сложных компоновок можно формировать виджеты упаковочных контейнеров, вложенные один в другой.

У типа GtkBox существуют два основных подкласса:

□ GtkHBox — однострочный горизонтальный упаковочный контейнер;

□ GtkVBox — одностолбцовый вертикальный упаковочный контейнер.

После создания упаковочных контейнеров следует задать два параметра: homogeneous и spacing:

GtkWidget* gtk_hbox_new(gboolean homogeneous, gint spacing);

GtkWidget* gtk_vbox_new(gboolean homogeneous, gint spacing);

Эти параметры управляют компоновкой всех виджетов в конкретном упаковочном контейнере. Параметр homogeneous — логический, если он равен TRUE, виджеты занимают одинаковую площадь независимо от их индивидуальных размеров. Параметр spacing задает расстояние между виджетами в пикселах.

После того как упаковочный контейнер создан, добавьте в него виджеты с помощью функций gtk_box_pack_start и gtk_box_pack_end:

void gtk_box_pack_start(GtkBox *box, GtkWidget *child,

 gboolean expand, gboolean f ill, guint padding);

void gtk_box_pack_end(GtkBox *box, GtkWidget *child,

 gboolean expand, gboolean fill, guint padding);

Функция gtk_box_pack_start вставляет виджеты, начиная от левого края контейнера GtkHBox и нижнего края контейнера GtkVBox; функция gtk_box_pack_end, наоборот, начинает от правого и верхнего краев контейнера. Параметры функций управляют расстоянием между виджетами и форматом каждого виджета, находящегося в упаковочном контейнере.

В табл. 16.1 описаны параметры, которые вы можете передавать в функцию gtk_box_pack_start или gtk_box_pack_end.

Таблица 16.1

Параметр Описание
GtkBox *box Заполняемый упаковочный контейнер
GtkWidget *child Виджет, который следует поместить в упаковочный контейнер
gboolean expand Если равен TRUE, данный виджет занимает все доступное пространство, используемое совместно с другими виджетами, у которых этот флаг также равен TRUE
gboolean fill Если равен TRUE, данный виджет будет занимать всю доступную площадь вместо использования ее как отступа от краев. Действует, только если флаг expand равен TRUE
guint padding Размер отступа вокруг виджета в пикселах

Давайте теперь рассмотрим эти виджеты упаковочных контейнеров и создадим более сложный пользовательский интерфейс, демонстрирующий вложенные упаковочные контейнеры (упражнение 16.3).

Упражнение 16.3. Макет виджета-контейнера

В этом примере вы спланируете размещение нескольких простых виджетов-меток типа GtkLabel с помощью контейнеров типа GtkHBox и GtkVBox. Виджеты-метки — простые графические элементы, подходящие для вывода коротких текстовых фрагментов. Назовите эту программу container.c:

#include <gtk/gtk.h>

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

/* Обратный вызов позволяет приложению отменить событие

   close/destroy. (Для отмены возвращает TRUE.) */

gboolean delete_event(GtkWidget *widget, GdkEvent *event, gpointer data) {

 printf("In delete_event\n");

 return FALSE;

}

int main (int argc, char *argv[]) {

 GtkWidget *window;

 GtkWidget *label1, *label2, *label3;

 GtkWidget *hbox;

 GtkWidget *vbox;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_window_set_title(GTK_WINDOW window), "The Window Title");

 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);

 gtk_window_set_default_size(GTK_WTNDOW(window), 300, 200);

 g_signal_connect(GTK_OBJECT(window), "destroy",

  GTK_SIGNAL_FUNC(closeApp), NULL);

 g_signal_connect(GTK_OBJECT(window), "delete_event",

  GTK_SIGNAL_FUNC(delete_event), NULL);

 label1 = gtk_label_new("Label 1");

 label2 = gtk_label_new("Label 2");

 label3 = gtk_label_new("Label 3");

 hbox = gtk_hbox_new(TRUE, 5);

 vbox = gtk_vbox_new(FALSE, 10);

 gtk_box_pack_start(GTK_BOX(vbox), label1, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(vbox), label2, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 5);

 gtk_container_add(GTK_CONTAINER(window), hbox);

 gtk_widget_show_all(window);

 gtk_main();

 return 0;

}

Когда вы выполните эту программу, то увидите следующую схему расположения виджетов-меток в вашем окне (рис. 16.6).

Рис. 16.6

Как это работает

Вы создаете два виджета упаковочных контейнеров: hbox и vbox. С помощью функции gtk_box_pack_start вы заполняете vbox виджетами label1 и label2, причем label2 располагается у нижнего края контейнера, потому что вставляется после label1. Далее контейнер vbox целиком наряду с меткой label3 вставляется в контейнер hbox.

В заключение hbox добавляется в окно и выводится на экран с помощью функции gtk_widget_show_all.

Схему размещения упаковочного контейнера легче понять с помощью блок-схемы, показанной на рис. 16.7.

Рис. 16.7

Познакомившись с виджетами, сигналами, обратными вызовами и виджетами-контейнерами, вы рассмотрели основы комплекта инструментов GTK+. Но для того чтобы стать программистом, профессионально применяющим GTK+, нужно понять, как наилучшим образом использовать имеющиеся в комплекте виджеты.

Виджеты GTK+

В этом разделе мы рассмотрим API самых популярных виджетов GTK+, которые вы будете применять чаще всего в своих приложениях.

GtkWindow

GtkWindow — базовый элемент всех приложений GTK+. До сих пор вы использовали его для хранения своих виджетов.

GtkWidget

 +---- GtkContainer

        +---- GtkBin

               +---- GtkWindow

Существуют десятки вызовов API GtkWindow, но далее приведены функции, заслуживающие особого внимания.

GtkWidget* gtk_window_new(GtkWindowType type);

void gtk_window_set_title(GtkWindow *window, const gchar *title);

void gtk_window_set_position(GtkWindow *window, GtkWindowPosition position);

void gtk_window_set_default_size(GtkWindow *window, gint width, gint height);

void gtk_window_resize(GtkWindow *window, gint width, gint height);

void gtk_window_set_resizable(GtkWindow *window, gboolean resizable);

void gtk_window_present(GtkWindow *window);

void gtk_window_maximize(GtkWindow *window);

void gtk_window_unmaximize(GtkWindow *window);

Как вы видели, функция gtk_window_new создает в памяти новое пустое окно. Заголовок окна не задан и размер и местоположение окна не определены. Обычно вы будете заполнять окно виджетами и задавать меню и панель инструментов перед выводом окна на экран с помощью вызова функции gtk_widget_show.

Функция gtk_window_set_title изменяет текст полосы заголовка, информируя оконный менеджер запроса.

Примечание

Поскольку за отображение оформления окна отвечает оконный менеджер, а не библиотека GTK+, шрифт, цвет и размер текста зависят от вашего выбора оконного менеджера.

Функция gtk_window_setposition управляет начальным местоположением на экране. Параметр position может принимать пять значений, перечисленных в табл. 16.2.

Таблица 16.2

Параметр position Описание
GTK_WIN_POS_NONE Окно располагается по усмотрению оконного менеджера
GTK_WIN_POS_CENTER Окно центрируется на экране
GTK_WIN_POS_MOUSE Расположение окна задаётся указателем мыши
GTK_WIN_POS_CENTER_ALWAYS Окно остается отцентрированным независимо от его размера
GTK_WIN_POS_CENTER_ON_PARENT Окно центрируется относительно родительского окна (удобно для диалоговых окон)

Функция gtk_window_set_default_size задает окно на экране в единицах отображения GTK+. Явное задание размера окна гарантирует, что содержимое окна не будет закрыто чем-либо или скрыто. Для того чтобы изменить размеры окна после его вывода на экран, можно воспользоваться функцией gtk_window_resize. По умолчанию пользователь может изменить размеры окна, перемещая обычным способом его границу мышью. Если вы хотите помешать этому, можно вызвать функцию gtk_window_set_resizeable, приравненную FALSE.

Для того чтобы убедиться в том, что ваше окно присутствует на экране и видно пользователю, т.е. не свернуто или скрыто, подойдет функция gtk_window_present. Она полезна для диалоговых окон, т.к. позволяет убедиться в том, что окна не свернуты, когда вам нужен какой-либо пользовательский ввод. В противном случае, для раскрытия окна на весь экран и его сворачивания у вас есть функции gtk_window_maximize и gtk_window_minimize.

GtkEntry

GtkEntry — виджет однострочного текстового поля, который обычно применяется для ввода простых текстовых данных, например, адреса электронной почты, имени пользователя или имени узла сети. Существуют вызовы API, позволяющие задать как считывание введенного текста, так и его максимальную длину в символах, а также другие параметры, управляющие местоположением текста и его выделением.

GtkWidget

 +----GtkEntry

Можно настроить GtkEntry на отображение звездочек (или любого другого определенного пользователем символа) на месте набранных буквенно-цифровых символов, что очень удобно для ввода паролей, когда вы не хотите, чтобы кто-то заглядывал через ваше плечо и читал текст.

Мы опишем большинство самых полезных функций виджета GtkEntry:

GtkWidget* gtk_entry_new(void);

GtkWidget* gtk_entry_new_with_max_length(gint max);

void gtk_entry_set_max_length(GtkEntry *entry, gint max);

G_CONST_RETURN gchar* gtk_entry_get_text(GtkEntry *entry);

void gtk_entry_set_text(GtkEntry *entry, const gchar *text);

void gtk_entry_append_text(GtkEntry *entry, const gchar *text);

void gtk_entry_prepend_text(GtkEntry* entry, const gchar *text);

void gtk_entry_set_visibility(GtkEntry *entry, gboolean visible);

void gtk_entry_set_invisible_char(GtkEntry *entry, gchar invch);

void gtk_entry_set_editable(GtkEntry *entry, gboolean editable);

Вы можете создать GtkEntry с помощью функции gtk_entry_new или при вводе текста фиксированной длины с помощью функции gtk_entry_new_with_max_length. Ограничение ввода определенной длиной текста избавляет вас от проверки корректности длины ввода и, возможно, необходимости информировать пользователя о том, что текст слишком длинный.

Для получения содержимого виджета GtkEntry вызывайте функцию gtk_entry_get_text, которая возвращает указатель const char, внутренний по отношению к GtkEntry (G_CONST_RETURN — макрос, определенный в библиотеке GLib). Если вы хотите изменить текст или передать его в функцию, которая может его модифицировать, следует скопировать строку с помощью, например, функции strcpy.

Вы можете вручную задавать и изменять содержимое виджета GtkEntry, применяя функции _set_text, _append_text и _modify_text. Учтите, что они принимают указатели const.

Для применения GtkEntry в качестве поля ввода пароля, которое отображает звездочки на месте символов, воспользуйтесь функцией gtk_entry_set_visibility, передав ей параметр visible со значением FALSE. Скрывающий символ можно изменить в соответствии с вашими требованиями с помощью функции gtk_entry_set_invisible_char.

Выполните упражнение 16.4.

Упражнение 16.4. Ввод имени пользователя или пароля

Теперь, познакомившись с функциями виджета GtkEntry, посмотрим на них в действии в небольшой программе. Программа entry.c будет создавать окно ввода имени пользователя и пароля и сравнивать введенный пароль с секретным.

1. Сначала определим секретный пароль, остроумно заданный как secret:

#include <gtk/gtk.h>

#include <stdio.h>

#include <string.h>

const char * password = "secret";

2. У вас есть две функции обратного вызова, которые вызываются, когда уничтожается окно и щелкается мышью кнопка OK:

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

void button_clicked(GtkWidget *button, gpointer data) {

 const char *password_text =

  gtk_entry_get_text(GTK_ENTRY((GtkWidget *) data));

 if (strcmp(password_text, password) == 0)

 printf("Access granted!\n");

 else printf("Access denied!\n");

}

3. В функции main создается, компонуется интерфейс и связываются обратные вызовы с сигналами. Для компоновки виджетов меток и полей ввода примените виджеты-контейнеры hbox и vbox:

int main (int argc, char *argv[]) {

 GtkWidget *window;

 GtkWidget *username_label, *password_label;

 GtkWidget *username_entry, *password_entry;

 GtkWidget *ok_button;

 GtkWidget *hbox1, *hbox2;

 GtkWidget *vbox;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_window_set_title(GTK_WINDOW(window), "GtkEntryBox");

 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);

 gtk_windowset_default_size(GTK_WINDOW(window), 200, 200);

 g_signal_connect(GTK_OBJECT(window), "destroy",

 GTK_SIGNAL_FUNC(closeApp), NULL);

 username_label = gtk_label_new("Login:");

 password_label = gtk_label_new("Password:");

 username_entry = gtk_entry_new();

 password_entry = gtk_entry_new();

 gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE);

 ok_button = gtk_button_new_with_label("Ok");

 g_signal_connect(GTK_OBJECT(ok_button), "clicked",

  GTK_SIGNAL_FUNC(button_clicked), password_entry);

 hbox1 = gtk_hbox_new(TRUE, 5);

 hbox2 = gtk_hbox_new(TRUE, 5);

 vbox = gtk_vbox_new(FALSE, 10);

 gtk_box_pack_start(GTK_BOX(hbox1), username_label, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox1), username_entry, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox2), password_label, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox2), password_entry, TRUE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(vbox), hbox1, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(vbox), hbox2, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(vbox), ck_button, FALSE, FALSE, 5);

 gtk_container_add(GTK_CONTAINER(window), vbox);

 gtk_widget_show_all(window);

 gtk_main();

 return 0;

}

Когда вы запустите программу, то получите окно, показанное на рис. 16.8.

Рис. 16.8 

Как это работает

Программа создает два виджета типа GtkEntry, username_entry и password_entry, а также задает видимость password_entry, равной FALSE, чтобы скрыть введенный пароль. Затем она формирует кнопку GtkButton, с помощью которой вы связываете сигнал clicked с функцией обратного вызова button_clicked.

Как только в функции обратного вызова программа извлечет введенный пароль и сравнит его с секретным паролем, на экран выводится соответствующее сообщение.

Обратите внимание на то, что для вставки виджетов в свои контейнеры вы много раз повторили операторы gtk_box_pack_start. Для сокращения этого повторяющегося программного кода в последующих примерах будет определена вспомогательная функция.

GtkSpinButton

Порой вам нужно, чтобы пользователь ввел числовое значение, например, максимальную скорость или размер инструмента, и в такой ситуации виджет GtkSpinButton (кнопка-счетчик) идеален. Он ограничивает ввод пользователя только цифровыми символами и можно задать диапазон допустимых значений от нижней до верхней границы. Виджет также содержит стрелки, направленные вверх и вниз, так что пользователь может "накручивать" значение, для удобства пользуясь только мышью.

GtkWidget

 +---- GtkEntry

        +---- GtkSpinButton

И снова API понятен, и мы перечислим наиболее часто применяемые вызовы:

GtkWidget* gtk_spin_button_new(GtkAdjustment *adjustment,

 gdouble climb_rate, guint digits);

GtkWidget* gtk_spin_button_new_with_range(gdouble min, gdouble max,

 gdouble step);

void gtk_spin_button_set_digits(GtkSpinButton *spin_button, guint digits);

void gtk_spin_button_set_increments(GtkSpinButton *spin_button,

 gdouble step, gdouble page);

void gtk_spin_button_set_range(GtkSpinButton *spin_button, gdouble min,

 gdouble max);

gdouble gtk_spin_button_get_value(GtkSpinButton *spin_button);

gint gtk_spin_button_get_value_as_int(GtkSpinButton *spin_button);

void gtk_spin_button_set_value(GtkSpinButton *spin button, gdouble value);

Для создания виджета GtkSpinButton с помощью функции gtk_spin_button_new вы сначала должны создать объект GtkAdjustment. Виджет GtkAdjustment — это абстрактный объект, содержащий логику, касающуюся управления значениями с ограничениями. Он также применяется и в других виджетах, таких как GtkHScale и GtkVScale.

Для создания объекта типа GtkAdjustment передайте в функцию нижнюю и верхнюю границы и размер приращения.

GtkObject* gtk_adjustment_new(gdouble value, gdouble lower,

 gdouble upper, gdouble step_increment,

 gdouble page_increment, gdouble page_size);

Значения параметров step_increment и page_increment задают величину минимального и максимального приращений, В случае кнопки-счетчика GtkSpinButton параметр step_increment определяет, насколько изменится значение при щелчке мышью стрелки виджета. Параметры page_increment и page_size в виджетах GtkSpinButton не важны.

Второй параметр, climb_rate, функции gtk_spin_button_new управляет скоростью прокрутки значений при нажатии и удерживании кнопки со стрелкой. И наконец, параметр digits задает точность представления числового значения, виджета, если, например, digits равен 3, кнопка-счетчик отобразит 0.00.

Функция gtk_spin_button_new_with_range — удобный способ создания объекта GtkAdjustment. Просто задайте нижнюю и верхнюю границы и величину приращения.

Прочесть текущее значение очень легко благодаря функции gtk_spin_button_getvalue, а если вам нужно целое число, можно применить функцию gtk_spin_button_get_value_as_int.

Выполните упражнение 16.5.

Упражнение 16.5. Использование виджета GtkSpinButton

Сейчас мы посмотрим в коротком примере, как действует кнопка-счетчик GtkSpinButton. Назовите файл spin.с.

#include <gtk/gtk.h>

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

int main(int argc, char *argv[]) {

 GtkWidget* window;

 GtkWidget *spinbutton;

 GtkObject *adjustment;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);

 g_signal_connect(GTK_OBJECT(window), "destroy",

  GTK_SIGNAL_FUNC(closeApp), NULL);

 adjustment = gtk_adjustment_new(100.0, 50.0, 150.0, 0.5, 0.05, 0.05);

 spinbutton = gtk_spin_button_new(GTK_ADJUSTMENT(adjustment), 0.01, 2);

 gtk_container_add(GTK_CONTAINER(window), spinbutton);

 gtk_widget_show_all(window);

 gtk_main();

 return 0;

}

Когда вы выполните программу, то получите кнопку-счетчик, ограниченную диапазоном значений 50–150 (рис. 16.9).

Рис. 16.9 

GtkButton

Вы уже видели виджет кнопки GtkButton в действии, но существует несколько виджетов, потомков GtkButton, с чуть большими функциональными возможностями, заслуживающими упоминания.

GtkButton

 +----GtkToggleButton

       +----GtkCheckButton

             +----GtkRadioButton

Как видно из иерархии виджетов, кнопка-переключатель типа GtkToggleButton — прямой потомок кнопки GtkButton, кнопка-флажок GtkCheckButton — кнопки-выключателя GtkToggleButton и то же самое для переключателя GtkRadioButton, причем каждый дочерний виджет предназначен для определенных задач.

GtkToggleButton

Виджет GtkToggleButton идентичен виджету GtkButton за исключением одной важной детали: GtkToggleButton обладает состоянием. Это означает, что кнопка-выключатель может быть включена или выключена. Когда пользователь щелкает мышью виджет GtkToggleButton, последний стандартным способом порождает сигнал clicked и изменяет (или "переключает") свое состояние.

API у виджета GtkToggleButton очень простой:

GtkWidget* gtk_toggle_button_new(void);

GtkWidget* gtk_toggle_button_new_with_label(const gchar* label);

gboolean gtk_toggle_button_get_active(GtkToggleButton *toggle_button);

void gtk_toggle_button_set_active(GtkToggleButton *toggle_button,

 gboolean is_active);

Наиболее интересные функции — gtk_toggle_button_get_active и gtk_toggle_button_set_active, которые вы вызываете для чтения и установки состояния кнопки-выключателя. Если характеристика функционирования равна TRUE, это означает, что кнопка-выключатель GtkToggleButton включена.

GtkCheckButton

Кнопка-флажок GtkCheckButton — это замаскированная кнопка-выключатель GtkToggleButton. Вместо скучного прямоугольного отображения GtkToggleButton кнопка GtkCheckButton выводится как привлекательный флажок с расположенным рядом текстом. Функциональных различий между ними нет.

GtkWidget* gtk_check_button_new(void);

GtkWidget* gtk_check_button_new_with_label(const gchar *label);

GtkRadioButton

Эта кнопка немного отличается от предыдущих, т.к. может группироваться с другими кнопками того же типа. Переключатель (или радиокнопка) GtkRadioButton — одна из тех кнопок, которые позволяют выбирать только один вариант из группы предложенных. Имя заимствовано у старых радиоприемников с механическими кнопками, которые выскакивали с треском, возвращаясь в прежнее состояние, при нажатии другой кнопки.

GtkWidget* gtk_radio_button_new(GSList *group);

GtkWidget* gtk_radio_button_new_from_widget(GtkRadioButton *group);

GtkWidget* gtk_radio_button_new_with_label(GSList *group, const gchar *label);

void gtk_radio_button_set_group(GtkRadioButton *radio_button, GSList *group);

GSList* gtk_radio_button_get_group(GtkRadioButton *radio_button);

Группа переключателей представлена в объекте-списке библиотеки GLib, названном GSList. Для того чтобы объединить переключатели в группу, вы можете создать объект GSList и затем передать ему каждую кнопку с помощью функций gtk_radio_button_new и gtk_radio_button_get_group, есть и более легкий способ в виде функции gtk_radio_button_new_with_widget, которая включает в GSList существующую кнопку. Вы увидите ее в действии в упражнении 16.6, которое позволит вам опробовать разные кнопки GtkButton.

Упражнение 16.6. GtkCheckButton, GtkToggleButton и GtkRadioButton

Введите следующий текст в файл с именем buttons.с.

1. Сначала объявите указатели на кнопки как глобальные переменные:

#include <gtk/gtk.h>

#include <stdio.h>

GtkWidget *togglebutton;

GtkWidget *checkbutton;

GtkWidget *radiobutton1, *radiobutton2;

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

2. Далее определите вспомогательную функцию, которая упаковывает GtkWidget и GtkLabel в контейнер GtkHbox и затем вставляет этот GtkHbox в заданный виджет- контейнер. Это поможет вам сократить повторяющийся программный код:

void add_widget_with_label(GtkContainer * box, gchar * caption,

 GtkWidget * widget) {

 GtkWidget *label = gtk_label_new(caption);

 GtkWidget *hbox = gtk_hbox_new(TRUE, 4);

 gtk_container_add(GTK_CONTAINER(hbox), label);

 gtk_container_add(GTK_CONTAINER(hbox), widget);

 gtk_container_add(box, hbox);

}

3. print_active — еще одна вспомогательная функция, которая выводит текущее состояние заданной кнопки-выключателя GtkToggleButton со строкой описания. Он вызывается из функции button_clicked, функции обратного вызова, связанной с сигналом clicked кнопки OK. При каждом щелчке мышью этой кнопки вы получаете на экране отчет о состоянии кнопок:

void print_active(char * button_name, GtkToggleButton* button) {

 gboolean active = gtk_toggle_button_get_active(button);

 printf("%s is %s\n", button_name, active?"active":"not active");

}

void button_clicked(GtkWidget *button, gpointer data) {

 print_active("Checkbutton", GTK_TOGGLE_BUTTON(checkbutton));

 print_active("Togglebutton", GTK_TOGGLE_BUTTON(togglebutton));

 print_active("Radiobutton1", GTK_TOGGLE_BUTTON(radiobutton1));

 print_active("Radiobutton2", GTK_TOGGLE_BUTTON(radiobutton2));

 printf("\n");

}

4. В функции main вы создаете виджеты кнопок, поочередно помещаете их в контейнер GtkVBox, добавив пояснительные метки, и связываете сигнал обратного вызова с кнопкой OK:

gint main(gint argc, gchar *argv[]) {

 GtkWidget* window;

 GtkWidget *button;

 GtkWidget *vbox;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_window_set_default_size(GTK_WINDOW(window), 200, 200);

 g_signal_connect(GTK_OBJECT(window), "destroy",

  GTK_SIGNAL_FUNC(closeApp), NULL);

 button = gtk_button_new_with_label("Ok");

 togglebutton = gtk_toggle_button_new_with_label("Toggle");

 checkbutton = gtk_check_button_new();

 radiobutton1 = gtk_radio_button_new(NULL);

 radiobutton2 =

  gtk_radio_button_new_from_widget(GTK_RADIO_BUTTON(radiobutton1));

 vbox = gtk_vbox_new(TRUE, 4);

 add_widget_with_label(GTK_CONTAINER(vbox), "ToggleButton:",

  togglebutton);

 add_widget_with_label(GTK_CONTAINER(vbox), "CheckButton:",

  checkbutton);

 add_widget_with_label(GTK_CONTAINER(vbox), "Radio 1:", radiobutton1);

 add_widget_with_label(GTK_CONTAINER(vbox), "Radio 2:", radiobutton2);

 add_widget_with_label(GTK_CONTAINER(vbox), "Button:", button);

 g_signal_connect(GTK_OBJECT(button), "clicked",

  GTK_SIGNAL_FUNC(button_clicked), NULL);

 gtk_container_add(GTK_CONTAINER(window), vbox);

 gtk_widget_show_all(window);

 gtk_main();

 return 0;

}

На рис. 16.10 показана программа buttons.c в действии с виджетами GtkButton четырех часто применяемых типов.

Рис. 16.10

Щелкните мышью кнопку OK, чтобы увидеть состояние разных кнопок.

Данная программа — простой пример использования кнопок GtkButton четырех типов — показывает, как можно считать состояние кнопки типа GtkToggleButton, GtkCheckButton и GtkRadioButton с помощью единственной функции gtk_toggle_button_get_active. Это одно из огромных преимуществ объектно-ориентированного подхода — поскольку вам не нужны отдельные функции get_active для каждого типа кнопки, вы можете сократить требующийся программный код.

GtkTreeView

К этому моменту мы рассмотрели несколько простых виджетов GTK+, но не все виджеты представляют собой однострочные инструменты для ввода или отображения. Сложность виджетов ничем не ограничивается, и GtkTreeView — яркий пример виджета, инкапсулирующего огромный объем функциональных возможностей.

GtkWidget

 +---- GtkContainer

        +---- GtkTreeView

GtkTreeView — член семейства виджетов, новых для комплекта GTK+ 2, создающий представление данных в виде дерева или списка наподобие тех, которые вы можете встретить в электронной таблице или файловом менеджере. С помощью виджета GtkTreeView можно создать сложные представления данных, смешивая текст, растровую графику и даже данные, вводимые с помощью виджетов GtkEntry, и т.д.

Самый быстрый способ испытания GtkTreeView — запуск приложения gtk-demo, которое поставляется вместе с GTK+. Демонстрационное приложение показывает возможности всех виджетов GTK+, включая GtkTreeView (рис. 16.11).

Рис. 16.11

Семейство GtkTreeView составляется из четырех компонентов:

□ GtkTreeView — отображение дерева или списка;

□ GtkTreeViewColumn — представление столбца списка или дерева;

□ GtkCellRenderer — управление отображаемыми ячейками;

□ GtkTreeModel — представление данных дерева и списка. 

Первые три компонента формируют так называемое Представление, а последний — Модель. Концепция разделения Представления и Модели (часто называемая проектным шаблоном Модель/Представление/Действие (Model/View/Controller) или сокращенно MVC) не свойственна GTK+, но проектированию уделяется все больше и больше внимания на всех этапах программирования.

Ключевое достоинство проектного шаблона MVC заключается в возможности одновременной визуализации данных в виде разных представлений без ненужного их дублирования. Например, текстовые редакторы могут иметь две разные панели и редактировать разные фрагменты документа без хранения в памяти двух копий документа.

Шаблон MVC также очень популярен в Web-программировании, поскольку облегчает создание Web-сайтов, которые визуализируются в мобильных или WAP-обозревателях не так, как в настольных, просто за счет наличия отдельных компонентов Представление, оптимизированных для Web-обозревателя каждого типа. Вы также можете отделить логику сбора данных, например, запросов к базе данных, от логики пользовательского интерфейса.

Мы начнем с рассмотрения компонента Модель, представленного в GTK+ двумя типами. Объект типа GtkTreeStore содержит многоуровневые данные, например иерархию каталогов, а объект GtkListStore предназначен для простых данных.

Для создания объекта GtkTreeStore в функцию передается количество столбцов, за которым следуют типы всех столбцов:

GtkWidget *store = gtk_tree_store_new(3, G_TYPE_STRING, G_TYPE_INT,

 G_TYPE_BOOLEAN);

Чтение, вставка, редактирование и удаление данных из модели выполняется с помощью структур GtkTreeIter. Эти структуры итераторов указывают на узлы дерева (или строки списка) и помогают находить фрагменты структур данных потенциально очень большого объема, а также манипулировать ими. Есть несколько вызовов API для получения объекта-итератора для разных точек дерева, но мы рассмотрим простейшую функцию gtk_tree_store_append.

Перед тем как вставлять какие-либо данные в модель дерева, вам нужно получить итератор, указывающий на новую строку. Функция gtk_tree_store_append заполняет объект GtkTreeIter, который представляет новую строку в дереве, как строку верхнего уровня (если вы передаете значение NULL в третьем аргументе), так и подчиненную или дочернюю строку (если вы передаете итератор главной или родительской строки):

GtkTreeIter iter;

gtk_tree_store_append(store, &iter, NULL);

Получив итератор, вы можете заполнять строку с помощью функции gtk_tree_store_set:

gtk_tree_store_set(store, &iter,

 0, "Def Leppard",

 1, 1987,

 2, TRUE, -1);

Номер столбца и данные передаются парами, которые завершаются -1. Позже вы примените тип enum для того, чтобы сделать номера столбцов более информативными.

Для того чтобы добавить ветвь к данной строке (дочернюю строку), вам нужен только итератор для дочерней строки, который вы получаете, вызвав снова функцию gtk_tree_store_append и указав на этот раз в качестве параметра строку верхнего уровня:

GtkTreeIter child;

gtk_tree_store_append(store, &child, &iter);

Дополнительную информацию об объектах GtkTreeStore и функциях объекта GtkListStore см. в документации API, а мы пойдем дальше и рассмотрим компонент Представление типа GtkTreeView.

Создание объекта GtkTreeView — сама простота: только передайте в конструктор в качестве параметра модель типа GtkTreeStore или GtkListStore:

GtkWidget* view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));

Сейчас самое время настроить виджет для отображения данных именно так, как вы хотите. Для каждого столбца следует определить GtkCellRenderer и источник данных. Можно выбрать, например, визуализацию только определенных столбцов данных или изменить порядок вывода столбцов.

GtkCellRenderer — это объект, отвечающий за прорисовку каждой ячейки на экране, и существует три подкласса, имеющие дело с текстовыми ячейками, ячейками пиксельной графики и ячейками кнопок-выключателей:

GtkCellRendererText;

GtkCellRendererPixBuf;

GtkCellRendererToggle.

В вашем Представлении будет применено текстовое представление ячеек, GtkCellRendererText.

GtkCellRenderer* renderer = gtk_cell_renderer_text_new();

gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),

 "This is the column title", renderer, "text", 0, NULL);

Вы создаете представление ячейки и передаете его в функцию вставки столбца. Эта функция позволяет сразу задать свойства GtkCellRendererText, передавая заканчивающиеся значением NULL пары "ключ/значение". В качестве параметров указаны представление дерева, номер столбца, заголовок столбца, представление ячейки и его свойства. В приведенном примере вы задаете атрибут "text", передав номер столбца источника данных. Для объекта GtkCellRendererText определено несколько других атрибутов, включая подчеркивание, шрифт, размер и т.д.

В упражнении 16.7, выполнив необходимые шаги, вы увидите, как это работает на практике.

Упражнение 16.7. Использование виджета GtkTreeView

Введите следующий программный код и назовите файл tree.с.

1. Примените тип enum для обозначения столбцов, чтобы можно было ссылаться на них по именам. Общее количество столбцов удобно обозначить как N_COLUMNS:

#include <gtk/gtk.h>

enum {

 COLUMN_TITLE, COLUMN_ARTIST, COLUMN_CATALOGUE, N_COLUMNS

};

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

int main(int argc, char *argv[]) {

 GtkWidget *window;

 GtkTreeStore *store;

 GtkWidget *view;

 GtkTreeIter parent_iter, child_iter;

 GtkCellRenderer *renderer;

 gtk_init(&argc, &argv);

 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);

 g_signal_connect(GTK_OBJECT(window), "destroy",

  GTK_SIGNAL_FUNC(сloseApp), NULL);

2. Далее вы создаете модель дерева, передавая количество столбцов и тип каждого из них:

 store = gtk_tree_store_new(N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING,

  G_TYPE_STRING);

3. Следующий этап — вставка родительской и дочерней строк в дерево:

 gtk_tree_store_append(store, &parent_iter, NULL);

 gtk_tree_store_set(store, &parent_iter,

  COLUMN_TITLE, "Dark Side of the Moon",

  COLUMN_ARTIST, "Pink Floyd",

  COLUMN_CATALOGUE, "B000024D4P", -1);

 gtk_tree_store_append(store, &child_iter, &parent_iter);

 gtk_tree_store_set (store, &child_iter,

  COLUMN_TITLE, "Speak to Me", -1);

 view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));

4. Наконец, добавьте столбцы в представление, задавая источники данных для них и заголовки:

 renderer = gtk_cell_renderer_text_new();

 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),

  COLUMN_TITLE, "Title", renderer, "text",

  COLUMN_TITLE, NULL);

 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),

  COLUMN_ARTIST, "Artist", renderer, "text",

  COLUMN_ARTIST, NULL);

 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),

  COLUMN_CATALOGUE, "Catalogue", renderer, "text",

  COLUMN_CATALOGUE, NULL);

 gtk_container_add(GTK_CONTAINER(window), view);

 gtk_widget_show_all(window); gtk_main();

 return 0;

}

Вы будете применять GtkTreeView как основной объект вашего приложения для работы с компакт-дисками, когда будете модифицировать содержимое GtkTreeView в соответствии с запросами к базе данных компакт-дисков.

Мы завершили обзор виджетов GTK+ и теперь обратим наше внимание на другую половину: среду GNOME. Вы увидите, как вставлять меню в ваше приложение с помощью библиотек GNOME и как виджеты GNOME облегчают программирование для рабочего стола GNOME.

Виджеты GNOME

Комплект GTK+ спроектирован как нейтральный по отношению к рабочему столу, т.е. GTK+ не делает никаких допущений о том, что он выполняется в среде GNOME или даже в системе Linux. Причина заключается в том, что комплект инструментов GTK+ можно с относительной легкостью перенести для выполнения в ОС Windows или любой другой оконной системе. В результате GTK+ не хватает средств для связывания программы с рабочим столом, таких как средства сохранения настройки программы, отображение файлов помощи или программные апплеты (апплеты — это небольшие утилиты, выполняющиеся на краевых панелях (edge panels)).

Библиотеки среды включают виджеты GNOME, расширяющие комплект GTK+ и замещающие его части более легкими в применении виджетами. В этом разделе мы расскажем, как программировать с помощью виджетов GNOME.

Перед использованием библиотек GNOME их следует инициализировать при запуске ваших программ точно так же, как вы поступали с библиотеками GTK+. Вы вызываете функцию gnome_program_init также, как вы вызывали функцию gtk_init в чистых программах GTK+.

Эта функция принимает параметры app_id и арр_version, применяемые для описания вашей программы в среде GNOME, module_info, сообщающий GNOME о том, какой библиотечный модуль инициализировать, параметры командной строки и свойства приложения, заданные как NULL-терминированный список пар "имя/значение".

GnomeProgram* gnome_program_init(const char *app_id,

 const char *app_version, const GnomeModuleInfо *module_infо,

 int argc, char **argv, const char *first_property_name, ...);

Необязательный список свойств позволяет задать такие характеристики, как, например, каталог для поиска растровой графики.

Выполните упражнение 16.8.

Упражнение 16.8. Окно GNOME

Давайте рассмотрим программу, применяющую средства GNOME, в которой выполняется GNOME-замещение объекта GtkWindow виджетом GnomeApp.

Введите эту программу и назовите ее gnome1.c:

#include <gnome.h>

int main(int argc, char* argv[]) {

 GtkWidget *app;

 gnome_program_init("gnome1", "1.0", MODULE, argc, argv, NULL);

 app = gnome_app_new("gnome1", "The Window Title");

 gtk_widget_show(app);

 gtk_main();

 return 0;

}

Для компиляции вам необходимо включить заголовочные файлы GNOME, поэтому передайте библиотеки libgnomeui и libgnome в команду pkg-config:

$ gcc gnome1.с -о gnome1 `pkg-config --cflags --libs libgnome-2.0 libgnomeui-2.0`

Виджет GnomeApp расширяет возможности GtkWindow и облегчает вставку меню, панелей инструментов и строки состояния вдоль нижнего края окна. Поскольку он потомок GtkWindow, вы можете применять к виджету GnomeApp любую функцию виджета GtkWindow. Далее вы познакомитесь с созданием меню и добавите строку состояния в ваш финальный пример.

Примечание

Вы можете использовать комплект инструментов GTK+ для создания меню, но среда GNOME предоставляет полезные структуры и макросы, которые существенно облегчают эту задачу. В интерактивной документации описывается, как создавать меню средствами GTK+.

Меню GNOME

Создание строки раскрывающихся меню в среде GNOME на удивление просто. Каждый пункт в строке меню представляется как массив структур GNOMEUIInfo, причем каждый элемент массива соответствует одному пункту меню. Например, если у вас есть меню File (Файл), Edit (Правка) и View (Вид), то у вас будут три массива, описывающих содержимое каждого меню.

После определения отдельных меню создается строка меню как таковая с помощью ссылок на эти массивы в еще одном массиве структур GNOMEUIInfo.

Структура GNOMEUIInfo немного сложна и нуждается в дополнительных пояснениях.

typedef struct {

 GnomeUIInfoType type;

 gchar const *label;

 gchar const *hint;

 gpointer moreinfо;

 gpointer user_data;

 gpointer unused_data;

 GnomeUIPixmapType pixmap_type;

 gconstpointer pixmap_info;

 guint accelerator_key;

 GdkModifierType ac_mods;

 GtkWidget *widget;

} GnomeUIInfo;

Первый элемент в структуре, type, определяет тип элемента меню, который описывается далее. Он может быть одним из 11 типов GnomeUIInfоТуре, определяемых средой GNOME и приведенных в табл. 16.3.

Таблица 16.3

Типы GnomeUIInfоТуре Описание
GNOME_APP_UI_ENDOFINFO Означает, что этот элемент — последний пункт меню в массиве
GNOME_APP_UI_ITEM Обычный пункт меню или переключатель, если ему предшествует элемент GNOME_APP_UI_RADIOITEMS
GNOME_APP_UI_TOGGLEITEM Пункт меню в виде кнопки-переключателя или кнопки-флажка
GNOME_APP_UI_RADIOITEMS Группа переключателей или зависимых переключателей
GNOME_APP_UI_SUBTREE Означает, что данный элемент представляет собой подменю. Задайте moreinfo для указания на массив подменю
GNOME_APP_UI_SEPARATOR Вставляет разделительную линию в меню
GNOME_APP_UI_HELP Создает список тем справки для использования в меню Help (Справка)
GNOME_APP_UI_BUILDER_DATA Задает данные построения (builder data) для следующих элементов
GNOME_APP_UI_ITEM_CONFIGURABLE Настраиваемый пункт меню
GNOME_APP_UI_SUBTREE_STOCK Такой же, как GNOME_APP_UI_SUBTREE за исключением того, что надписи следует искать в каталоге gnome-libs
GNOME_APP_UI_INCLUDE Такой же, как GNOME_APP_UI_SUBTREE за исключением того, что пункты включены в текущее меню, а не в подменю

Второй и третий элементы структуры определяют текст пункта меню и всплывающей подсказки. (Подсказка выводится в строке состояния, у нижнего края окна.)

Назначение элемента moreinfo зависит от типа. В случае ITEM и TOGGLEITEM он указывает на функцию обратного вызова, которую следует вызвать при активации пункта меню. Для RADIOITEMS он указывает на массив структур GnomeUIInfo, в которых группируются переключатели.

user_data — произвольный указатель, передаваемый в функцию обратного вызова. Элементы pixmap_type и pixmap_info позволяют добавить к пункту меню растровую пиктограмму, a accelerator_key и ac_mods помогут определить клавиатурный эквивалент пункта меню.

И наконец, элемент widget применяется для внутреннего хранения указателя на виджет пункта меню функцией создания меню.

Выполните упражнение 16.9.

Упражнение 16.9. Меню GNOME

Вы сможете опробовать меню с помощью данной короткой программы. Назовите ее menu1.с.

#include <gnome.h>

void closeApp(GtkWidget *window, gpointer data) {

 gtk_main_quit();

}

1. Определите для пунктов меню функцию обратного вызова, названную item_clicked:

void item clicked(GtkWidget *widget, gpointer user_data) {

 printf("Item Clicked!\n");

}

2. Далее следуют определения меню. У вас есть подменю, меню верхнего уровня и массив строки меню:

static GnomeUIInfo submenu[] = {

 {GNOME_APP_UI_ITEM, "SubMenu", "SubMenu Hint",

  GTK_SIGNAL_FUNC(item_clicked), NULL, NULL, 0, NULL, 0, 0, NULL},

 {GNOME_APP_UI_ENDOFINFO, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 0,

  NULL}

};

static GnomeUIInfo menu[] = {

 {GNOME_APP_UI_ITEM, "Menu Item 1", "Menu Hint",

  NULL, NULL, NULL, 0, NULL, 0, 0, NULL},

 {GNOME_APP_UI_SUBTREE, "Menu Item 2", "Menu Hint",

  submenu, NULL, NULL, 0, NULL, 0, 0, NULL},

 {GNOME_APP_UI_ENDOFINFO, NULL, NULL, null,

  NULL, NULL, 0, NULL, 0, 0, NULL}

};

static GnomeUIInfo menubar[] = {

 {GNOME_APP_UI_SUBTREE, "Toplevel Item", NULL,

  menu, NULL, NULL, 0, NULL, 0, 0, NULL},

 {GNOME_APP_UI_ENDOFINFO, NULL, NULL, NULL,

  NULL, NULL, 0, NULL, 0, 0, NULL}

};

3. В функции main вы имеете дело с обычной инициализацией и затем создаете ваш виджет GnomeApp и задаете все меню:

int main (int argc, char *argv[]) {

 GtkWidget *app;

 gnome_program_init("gnome1", "0.1", LIBGNOMEUI_MODULE,

  argc, argv, GNOME_PARAM_NONE);

 app = gnome_app_new("gnome1", "Menus, menus, menus");

 gtk_window_set_default_size(GTK_WINDOW(app), 300, 200);

 g_signal_connect(GTK_OBJECT(app), "destroy",

  GTK_SIGNAL_FUNC(closeApp), NULL);

 gnome_app_create_menus(GNOME_APP(app), menubar);

 gtk_widget_show(app);

 gtk_main();

 return 0;

}

Попробуйте выполнить menu1 и посмотрите в действии строку меню, подменю и меню GNOME обратного вызова, показанные на рис. 16.12.

Рис. 16.12

Структура GnomeUIInfo едва ли дружественная по отношению к программисту, если учесть, что она состоит из 11 элементов, большинство из которых обычно равно NULL или нулю. При их вводе очень легко допустить ошибку и трудно отличить одно поле от другого в длинном массиве элементов. Для улучшения сложившейся ситуации в среде GNOME определены макросы, устраняющие необходимость определения структур вручную. Эти макросы также вставляют пиктограммы и клавиатурные акселераторы для вас, и все даром. На самом деле редко возникают причины, заставляющие использовать вместо них что-то другое.

Существуют два набора макросов, первый из которых определяет отдельные пункты меню. Эти макросы принимают два параметра: указатель на функцию обратного вызова и данные пользователя.

#include <libgnomeui/libgnameui.h>

#define GNOMEUIINFO_MENU_OPEN_ITEM(cb, data)

#define GNOMEUIINFO_MENU_SAVE_ITEM(cb, data)

#define GNOMEUIINFO_MENU_SAVE_AS_IТЕМ(cb, data)

#define GNOMEUIINFO_MENU_PRINT_ITEM(cb, data)

#define GNOMEUIINFO_MENU_PRINT_SETUP_ITEM(cb, data)

#define GNOMEUIINFO_MENU_CLOSE_IТЕМ(cb, data)

#define GNOMEUIINFO_MENU_EXIT_IТЕМ(cb, data)

#define GNOMEUIINFO_MENU_QUIT_IТЕМ(cb, data)

#define GNOMEUIINFO_MENU_CUT_ITEM(cb, data)

#define GNOMEUIINFO_MENU_COPY_ITEM(cb, data)

#define GNOMEUIINFO_MENU_PASTE_ITEM(cb, data)

#define GNOMEUIINFO_MENU_SELECT_ALL_ITEM(cb, data)

...

Второй набор предназначен для определений верхнего уровня, в него вы просто передаете массив.

#define GNOMEUIINFO_MENU_FILE_TREE     (tree)

#define GNOMEUIINFO_MENU_EDIT_TREE     (tree)

#define GNOMEUIINFO_MENU_VIEW_TREE     (tree)

#define GNOMEUIINFO_MENU_SETTINGS_TREE (tree)

#define GNOMEUIINFO_MENU_FILES_TREE    (tree)

#define GNOMEUIINFO_MENU_WINDOWS_TREE  (tree)

#define GNOMEUIINFO_MENU_HELP_TREE     (tree)

#define GNOMEUIINFO_MENU_GAME_TREE     (tree)

Выполните упражнение 16.10.

Упражнение 16.10. Меню с помощью макросов GNOME

В этом примере вы воспользуетесь уже заданными меню и посмотрите, как работают макросы. Внесите следующие изменения в программу menu1.с и назовите новый вариант menu2.c. Для простоты в этом примере для пунктов меню не определены функции обратного вызова. В данном случае наша задача — просто продемонстрировать удобство применения макросов GNOME, формирующих меню.

#include <gnome.h>

static GnomeUIInfo filemenu[] = {

 GNOMEUIINFO_MENU_NEW_ITEM("New", "Menu Hint", NULL, NULL),

 GNOMEUIINFO_MENU_OPEN_ITEM(NULL, NULL),

 GNOMEUIINFO_MENU_SAVE_AS_ITEM(NULL, NULL),

 GNOMEUIINFO_SEPARATOR,

 GNOMEIINFO_MENU_EXIT_ITEM(NULL, NULL),

 GNOMEUUINFO_END

};

static GnomeUUInfo editmenu[] =

 GNOMEUIINFO_MENU_FIND_ITEM(NULL, NULL),

 GNOMEUIINFO_END

};

static GnomeUIInfo menubar[] = {

 GNOMEUIINFO_MENU_FILE_TREE(filemenu),

 GNOMEUIINFO_MENU_EDIT_TREE(editmenu),

 GNOMEUIINFO_END

};

int main(int argc, char *argv[]) {

 GtkWidget *app, *toolbar;

 gnome_program_init("gnome1", "0.1", LIBGNOMEUI_MODULE,

  argc, argv, GNOME_PARAM_NONE);

 app = gnome_app_new("gnome1", "Menus, menus, menus");

 gtk_window_set_default_size(GTK_WINDOW(app), 300, 200);

 gnome_app_create_menus(GNOME_APP(app), menubar);

 gtk_widget_show(app);

 gtk_main();

 return 0;

}

Применив макросы libgnomeui в menu2.c, вы значительно сократили код, который нужно набирать, и сделали его гораздо понятнее. Макросы экономят ваше время и усилия, предпринимаемые для создания меню и согласования текста меню, клавиатурных акселераторов и пиктограмм с другими приложениями GNOME. Старайтесь применять их в ваших приложениях при любой возможности.

На рис. 16.13 показана программа menu3.c в действии на сей раз со стандартизованными в среде GNOME пунктами меню.

Рис. 16.13

Диалоговые окна

Основная часть любого приложения GUI — взаимодействие с пользователем и информирование его о важных событиях. Обычно для этого вы создаете временное окно с кнопками OK и Cancel и, если информация настолько важна, что требует немедленного отклика, например удаление файла, вам приходится блокировать доступ ко всем остальным окнам до тех пор, пока пользователь не сделает выбор (такие окна называют модальными диалоговыми окнами).

Мы только что описали диалоговое окно, и в комплекте GTK+ есть специальные виджеты диалоговых окон, являющиеся потомками виджета GtkWindow, что существенно облегчает вашу программистскую работу.

GtkDialog

Как вы можете видеть, объект GtkDialog — потомок объекта GtkWindow и наследует все его функции и свойства.

GtkWindow

 +----GtkDialog

GtkDialog делит окно на две области, одна для содержимого виджета и другая для кнопок, которые располагаются вдоль нижнего края окна. Вы можете задать нужные вам кнопки и другие параметры диалогового окна во время его создания.

GtkWidget* gtk_dialog_new_with_buttons(const gchar *title,

 GtkWindow *parent, GtkDialogFlags flags,

 const gchar *first button text, ...);

Эта функция создает диалоговое окно с заголовком и кнопками. Второй параметр, parent, должен указывать на главное окно вашего приложения, чтобы комплект GTK+ мог убедиться в том, что диалоговое окно остается присоединенным к главному окну и минимизируется при сворачивании главного окна.

Параметр flags определяет комбинацию свойств диалогового окна:

GTK_DIALOG_MODAL;

GTK_DIALOG_DESTROY_WITH_PARENT;

GTK_DIALOG_NO_SEPARATOR.

Вы можете комбинировать флаги с помощью поразрядной операции OR; например, комбинация GTK_DIALOG_MODAL | GTK_DIALOG_NO_SEPARATOR означает одновременно и модальное окно, и окно без разделительной линии между основной областью окна и областью кнопок.

Оставшиеся параметры — это NULL-терминированный список кнопок и код соответствующего отклика. Вы поймете, что именно означает этот код отклика, когда познакомитесь с функцией gtk_dialog_run. Обычно кнопки выбираются из длинного списка готовых кнопок, которые определяет GTK+, поскольку вы получите уже готовые пиктограммы в кнопках.

Далее показано, как бы вы создавали диалоговое окно с кнопками OK и Cancel, которое возвращает GTK_RESPONSE_ACCEPT и GTK_RESPONSE_REJECT при нажатии этих кнопок:

GtkWidget *dialog = gtk_dialog_new_with_buttons("Important question",

 parent_window,

 GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_OK,

 GTK_RESPONSE_ACCEPT, GTK_STOCK_CANCEL,

 GTK_RESPONSE_REJECT, NULL);

Мы остановились на двух кнопках, но на самом деле на количество кнопок в диалоговом окне нет ограничений. Более того, вы можете выбирать из ряда флагов типа отклика. Флаги accept (принять) и reject (отвергнуть) не применяются в стандарте GNOME и могут использоваться в ваших приложениях по вашему усмотрению. (Помните о том, что accept в вашем приложении должен означать "принять".) Другие варианты, включая отклик OK и CANCEL, приведены в типе GtkResponseType enum в следующем разделе.

Естественно, вы должны вставить содержимое в ваше диалоговое окно и для этого объект GtkDialog содержит готовый упаковочный контейнер GtkVBox для заполнения виджетами. Вы получаете указатель прямо из объекта:

GtkWidget *vbox = GTK_DIALOG(dialog)->vbox;

Этот GtkVBox применяется обычным способом с помощью функции gtk_box_pack_start или чего-то подобного.

После того как диалоговое окно создано, следующий шаг — представить его пользователю и ждать от него ответа. Сделать это можно двумя способами: в модальном режиме, который блокирует весь ввод за исключением диалогового окна, или в немодальном режиме, который воспринимает диалоговое окно как любое другое окно. Давайте сначала рассмотрим запуск модального диалогового окна.

Модальное диалоговое окно

Модальное диалоговое окно заставляет пользователя ответить до того, как сможет выполниться любое другое действие. Оно полезно в тех ситуациях, когда пользователь собирается сделать что-то, сопряженное с серьезными последствиями, или нужно вывести сообщения об ошибках и предупреждениях.

Диалоговое окно можно сделать модальным, установив флаг GTK_DIALOG_MODAL и вызвав функцию gtk_widget_show, но есть лучший путь. Функция gtk_dialog_run выполнит за вас всю тяжелую работу, остановив дальнейшее выполнение программы до тех пор, пока не будет нажата кнопка в диалоговом окне.

Когда пользователь нажимает кнопку (или диалоговое окно уничтожается), функция gtk_dialog_run возвращает результат типа int, указывающий на кнопку, нажатую пользователем. В GTK+ очень кстати определен тип enum для описания возможных значений.

typedef enum {

 GTK_RESPONSE_NONE = -1,

 GTK_RESPONSE_REJECT = -2,

 GTK_RESPONSE_ACCEPT = -3,

 GTK_RESPONSE_DELETE_EVENT = -4

 GTK_RESPONSE_OK = -5,

 GTK_RESPONSE_CANCEL = -6,

 GTK_RESPONSE_CLOSE = -7,

 GTK_RESPONSE_YES = -8,

 GTK_RESPONSE_NO = -9,

 GTK_RESPONSE_APPLY = -10,

 GTK_RESPONSE_HELP = -11

} GtkResponseType;

Теперь мы можем объяснить код отклика, передаваемый в функцию gtk_dialog_new_with_buttons, — это код возврата типа GtkResponseType, который функция gtk_dialog_run возвращает, когда нажата кнопка. Если диалоговое окно уничтожается (это происходит, например, когда пользователь щелкает кнопкой мыши пиктограмму закрытия), вы получаете результат GTK_RESPONSE_NONE.

Для вызова соответствующих операторов идеально подходит конструкция switch:

GtkWidget* dialog = create_dialog();

int result = gtk_dialog_run(GTK_DIALOG(dialog));

switch(result) {

case GTK_RESPONSE_ACCEPT:

 delete_file();

 break;

сазе GTK_RESPONSE_REJECT:

 do_nothing();

 break;

default:

 dialog_was_cancelled();

 break;

}

gtk_widget_destroy(dialog);

Это все, что есть для простых модальных окон в комплекте инструментов GTK+. Как видите, включен очень небольшой программный код и потрачено немного усилий. В конце нужно только провести чистку с помощью функции gtk_widget_destroy.

Если вам понадобится немодальное диалоговое окно, все будет не так просто. Вы не сможете использовать функцию gtk_dialog_run, вместо нее придется связать функции обратного вызова с кнопками диалогового окна.

Немодальные диалоговые окна

Мы рассмотрели, как применять функцию gtk_dialog_run для создания модального (блокирующего) диалогового окна. Немодальное окно действует несколько иначе, хотя и создается тем же способом. Вместо вызова функции gtk_dialog_run вы связываете функцию обратного вызова с сигналом отклика объекта GtkDialog, который генерируется при щелчке кнопки мышью или уничтожении окна.

Связывание сигнала обратного вызова выполняется обычным образом с той лишь разницей, что у функции обратного вызова появляется дополнительный аргумент отклика, играющий ту же роль, что код возврата функции gtk_dialog_run. В приведенном далее фрагменте программного кода показаны основные принципы использования немодального диалогового окна:

void dialog_button_clicked(GtkWidget *dialog, gint response,

 gpointer user_data) {

 switch (response) {

 case GTK_RESPONSE_ACCEPT:

  do_stuff();

  break;

 case GTK_RESPONSE_REJECT:

  do_nothing();

  break;

 default:

  dialog_was_cancelled();

  break;

 }

 gtk_widget_destroy(dialog);

}

int main() {

 ...

 GtkWidget *dialog = create_dialog();

 g_signal_connect(GTK_OBJECT(dialog), "response",

  GTK_SIGNAL_FUNC(dialog_button_clicked), user_data);

 gtk_widget_show(dialog);

 ...

}

С немодальными диалоговыми окнами могут возникать сложности, т.к. от пользователя не требуется немедленного ответа, и он может свернуть диалоговое окно и забыть о нем. Вы должны предусмотреть действия при попытке пользователя повторно открыть диалоговое окно до закрытия первого экземпляра окна. Следует проверить, равен ли NULL указатель диалогового окна и если нет, повторно вывести уже открытое диалоговое окно на экран, вызвав функцию gtk_window_present. Вы увидите этот прием в действии в разд. "Приложение для работы с базой данных компакт-дисков" в конце данной главы.

GtkMessageDialog

Для очень простых диалоговых окон даже тип GtkDialog излишне сложен.

GtkDialog

 +----GtkMessageDialog

С помощью типа GtkMessageDialog вы можете создать информационное диалоговое окно одной строкой программного кода.

GtkWidget* gtk_message_dialog_new(GtkWindow *parent,

 GtkDialogFlags flags, GtkMessageType type,

 GtkButtonsType buttons, const gchar *message_format, ...);

Эта функция создает диалоговое окно, снабженное пиктограммами, заголовком и настраиваемыми кнопками. Параметр type задает готовую пиктограмму и заголовок диалогового окна в соответствии с его предполагаемым назначением; например, окно с предупреждением содержит пиктограмму предупреждения в виде треугольника. Существует четыре возможных варианта для простых диалоговых окон, с которыми вы будете сталкиваться чаще всего:

GTK_MESSAGE_INFO;

GTK_MESSAGE_WARNING;

GTK_MESSAGE_QUESTION;

GTK_MESSAGE_ERROR.

Вы также можете выбрать значение GTK_MESSAGE_OTHER, применяемое в тех случаях, когда не используются перечисленные типы. Для окна типа GtkMessageDialog можно передать тип GtkButtonsType (табл. 16.4) вместо перечисления всех кнопок по очереди.

Таблица 16.4

Тип GtkButtonsType Описание
GTK_BUTTONS_OK Кнопка OK
GTK_BUTTONS_CLOSE Кнопка Close
GTK_BUTTONS_CANCEL Кнопка Cancel
GTK_BUTTONS_YES_NO Кнопки Yes и No
GTK_BUTTONS_OK_CANCEL Кнопки OK и Cancel
GTK_BUTTONS_NONE Нет кнопок

Теперь остается только текст диалогового окна, который можно создать из строки подстановки, формируемой так же, как в функции printf. В данном примере вы спрашиваете пользователя, настаивает ли он на своем требовании удалить файл:

GtkWidget *dialog = gtk_message_dialog_new(main_window,

 GTK_DIALOG_DESTROY_WITH_PARENT,

 GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,

 "Are you sure you wish to delete %s?", filename);

result = gtk_dialog_run(GTK_DIALOG(dialog));

gtk_widget_destroy(dialog);

Это диалоговое окно будет отображаться так, как показано на рис. 16.14.

Рис. 16.14

Окно типа GtkMessageDialog — простейший способ обмена информацией или получения ответов на вопросы типа "да/нет". Вы воспользуетесь им в следующем разделе, когда примените полученные знания для создания GUI вашего приложения для работы с базой данных компакт-дисков.

Приложение для работы с базой данных компакт-дисков

В предыдущих главах вы разрабатывали базу данных компакт-дисков с помощью MySQL и интерфейса на языке С. Теперь вы увидите, как просто вставить внешний GUI средствами GNOME/GTK+ и создать пользовательский интерфейс с богатыми функциональными возможностями. 

Примечание

Для проверки примера приложения для работы с базой данных компакт-дисков у вас должны быть установлены СУБД MySQL и библиотеки разработки, т.е. должны выполняться те же самые требования, что и к аналогичному приложению в главе 8.

Из соображений простоты и ясности мы создадим базовый скелетный интерфейс, в котором реализовано лишь подмножество функций — к примеру, вы не сможете добавлять информацию о дорожках в компакт-диски или удалять CD. Но вы увидите в вашем приложении в действии виджеты, обсуждавшиеся в этой главе, и поймете, как они применяются в реальных программах.

Будет написан программный код для следующих ключевых действий:

□ регистрация в базе данных из GUI;

□ поиск компакт-диска;

□ отображение сведений о компакт-диске и его дорожках;

□ вставка компакт-диска в базу данных;

□ создание окна About (О программе);

□ формирование подтверждения при завершении работы пользователя.

Разделим код на три файла, совместно использующие заголовочный файл cdapp_gnome.h. В исходных файлах функции создания окон и диалоговых окон — функции формирования интерфейса — отделены от функций обратного вызова (упражнения 16.11-16.14).

Упражнение 16.11. Файл cdapp_gnome.h

Сначала рассмотрим файл cdapp_gnome.h и функции, которые вы должны реализовать.

1. Включите в исходный текст программы заголовочные файлы среды GNOME и заголовочный файл для функций интерфейса, разработанного вами в главе 8. В данном примере программы используются файлы app_mysql.h и app_mysql.c из главы 8 и созданная там же база данных.

#include <gnome.h>

#include "app_mysql.h"

2. В типе enum обозначены столбцы виджета GtkTreeView, который вы будете применять для отображения сведений о компакт-дисках и их дорожках. 

enum {

 COLUMN_TITLE,

 COLUMN_ARTIST,

 COLUMN_CATALOGUE,

 N_COLUMNS

};

3. У вас есть три функции создания окна в файле interface.c.

GtkWidget *create_main_window();

GtkWidget *create_login_dialog();

GtkWidget *create_addcd_dialog();

4. Функции обратного вызова для пунктов меню, панели инструментов, кнопок диалогового окна и кнопки поиска находятся в файле callbacks.с.

/* Обратный вызов для выхода из приложения */

void quit_app(GtkWidget* window, gpointer data);

/* Обратный вызов для подтверждения завершения перед выходом */

gboolean delete_event_handler(GtkWidget* window, GdkEvent *event,

 gpointer data);

/* Обратный вызов, связанный с сигналом отклика диалогового окна addcd */

void addcd_dialog_button_clicked(GtkDialog * dialog, gint response,

 gpointer userdata);

/* Обратный вызов для кнопки Add CD меню и панели инструментов */

void on_addcd_activate(GtkWidget *widget, gpointer user_data);

/* Обратный вызов для кнопки меню About */

void on_about_activate(GtkWidget* widget, gpointer user_data);

/* Обратный вызов для кнопки поиска */

void on_search_button_clicked(GtkWidget *widget, gpointer userdata);

Упражнение 16.12. Файл interface.c

Первым рассмотрим файл interface.c, в котором определяются окна и диалоговые окна, применяемые в приложении.

1. Сначала несколько указателей виджетов, на которые вы ссылаетесь в файлах callbacks.c и main.c:

#include "app_gnome.h"

GtkWidget* treeview;

GtkWidget* appbar;

GtkWidget* artist_entry;

GtkWidget *title_entry;

GtkWidget *catalogue_entry;

GtkWidget *username_entry;

GtkWidget *password_entry;

2. app — глобальная переменная, указатель на главное окно:

static GtkWidget *арр;

3. Определите вспомогательную функцию, которая вставляет в контейнер виджет-метку с заданным текстом:

void add_widget_with_label(GtkContainer *box,

 gchar *caption, GtkWidget *widget) {

 GtkWidget *label = gtk_label_new(caption);

 GtkWidget *hbox = gtk_hbox_new(TRUE, 4);

 gtk_container_add(GTK_CONTAINER(hbox), label);

 gtk_container_add(GTK_CONTAINER(hbox), widget);

 gtk_container_add(box, hbox);

}

4. Определения строки меню, использующие для удобства макросы GNOMEUIINFO:

static GnomeUIInfo filemenu[] = {

 GNOMEUIINFO_MENU_NEW_ITEM("_New CD", NULL, on_addcd_activate, NULL),

 GNOMEUIINFO_SEPARATOR,

 GNOMEUIINFO_MENU_EXIT_ITEM(close_app, NULL),

 GNOMEUIINFO_END

};

static GnomeUIInfo helpmenu[] = {

 GNOMEUIINFO_MENU_ABOUT_ITEM(on_about_activate, NULL),

 GNOMEUIINFO_END

};

static GnomeUIInfo menubar[] = {

 GNOMEUIINFO_MENU_FILE_TREE(filemenu),

 GNOMEUIINFO_MENU_HELP_TREE(helpmenu),

 GNOMEUIINFO_END

};

5. Теперь вы создаете главное окно, вставляете меню и панель инструментов, задаете их размер, центрируете относительно экрана и собираете виджеты, формирующие интерфейс. Учтите, что эта функция не отображает окно на экране, а просто возвращает указатель на окно:

GtkWidget *create_main_window() {

 GtkWidget* toolbar;

 GtkWidget* vbox;

 GtkWidget* hbox;

 GtkWidget* label;

 GtkWidget* entry;

 GtkWidget *search_button;

 GtkWidget* scrolledwindow;

 GtkCellRenderer *renderer;

 app = gnome_app_new("GnomeCD", "CD Database");

 gtk_window_set_position(GTK_WINDOW(app), GTK_WIN_POS_CENTER);

 gtk_window_set_defeult_size(GTK_WINDOW(app ), 540, 480);

 gnome_app_create_menus(GNOME_APP(app), menubar);

6. Создайте панель инструментов с помощью стандартных пиктограмм GTK+ и свяжите с ней функции обратного вызова:

 toolbar = gtk_toolbar_new();

 gnome_app_add_toolbar(GNOME_APP(app), GTK_TOOLBAR(toolbar),

  "toolbar", BONOBO_DOCK_ITEM_BEH_EXCLUSIVE,

  BONOBO_DOCK_TOP, 1, 0, 0);

 gtk_container_set_border_width(GTK_CONTAINER(toolbar), 1);

 gtk_toolbar_insert_stock(GTK_TOOLBAR(toolbar), "gtk-add", "Add new CD",

  NULL, GTK_SIGNAL_FUNC(on_addcd_activate), NULL, -1);

 gtk_toolbar_insert_space(GTK_TOOLBAR(toolbar), 1);

 gtk_toolbar_insert_stock(GTK_TOOLBAR(toolbar), "gtk-quit",

  "Quit the Application", NULL, GTK_SIGNAL_FUNC(on_quit_activate), NULL, -1);

7. Затем вы создаете виджеты, используемые для поиска компакт-диска:

 label = gtk_label_new("Search String:");

 entry = gtk_entry_new();

 search_button = gtk_button_new_with_label("Search");

8. Окно gtk_scrolled_window предоставляет полосы прокрутки, позволяя виджету (в данном случае GtkTreeView) превышать размеры окна:

 scrolledwindow = gtk_scrolled_window_new(NULL, NULL);

 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow),

  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

9. Далее скомпонуйте интерфейс, применяя стандартным способом виджеты-контейнеры:

 vbox = gtk_vbox_new(FALSE, 0);

 hbox = gtk_hbox_new(FALSE, 0);

 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 6);

 gtk_box_pack_start(GTK_BOX(hbox), search_button, FALSE, FALSE, 5);

 gtk_box_pack_start(GTK_BOX(vbox), scrolledwindow, TRUE, TRUE, 0);

10. Затем создайте виджет GtkTreeView, вставьте три столбца и поместите его в окно GtkScrolledWindow:

 treeview = gtk_tree_view_new();

 renderer = gtk_cell_renderer_text_new();

 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview),

  COLUMN_TITLE, "Title", renderer, "text", COLUMN_TITLE, NULL);

 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview),

  COLUMN_ARTIST, "Artist", renderer, "text", CQLUMN_ARTIST, NULL);

 gtk_tree_view_insert_column_with_attrihutes(GTK_TREE_VIEW(treeview),

  COLUMN_CATALOGUE, "Catalogue", renderer, "text", COLUMN_CATALOGUE, NULL);

 gtk_tree_view_set_search_column(GTK_TREE_VIEW(treeview),

  COLUMN_TITLE);

 gtk_container_add(GTK_CONTAINER(scrolledwindow), treeview);

11. В заключение задайте содержимое главного окна, вставьте строку состояния GnomeApp и подсоедините нужные обратные вызовы:

 gnome_app_set_contents(GNOMEAPP(app), vbox);

 appbar = gnome_appbar_new(FALSE, TRUE, GNOME_PREFERENCES_NEVER);

 gnome_app_set_statusbar(GNOME_APP(app), appbar);

 gnome_app_install_menu_hints(GNOME_APP(app), menubar);

 g_signal_connect(GTK_OBJECT(search_button), "clicked",

  GTK_SIGNAL_FUNC(on_search_button_clicked), entry);

 g_signal_connect(GTK_OBJECT(app), "delete_event",

  GTK_SIGNAL_FUNC(delete_event_handler), NULL);

 g_signal_connect(GTK_OBJECT(app), "destroy",

  GTK_SIGNAL_FUNC(quit_app), NULL);

 return app;

}

12. Следующая функция создает простое диалоговое окно, позволяющее добавлять новый компакт-диск в базу данных. Оно состоит из полей ввода для исполнителя, названия и полей каталога, а также кнопок OK и Cancel:

GtkWidget *create_addcd_dialog() {

 artist_entry = gtk_entry_new();

 title_entry = gtk_entry_new();

 catalogue_entry = gtk_entry_new();

 GtkWidget* dialog = gtk_dialog_new_with_buttons("Add CD",

  app,

  GTK_DIALOG_DESTROY_WITH_PARENT,

  GTK_STOCK_OK,

  GTK_RESPONSE_ACCEPT,

  GTK_STOCK_CANCEL,

  GTK_RESPONSE_REJECT,

  NULL);

 add_widget_with_label(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),

  "Artist", artist_entry);

 add_widget_with_label(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),

  "Title", title_entry);

 add_widget_with_label(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),

  "Catalogue", catalogue_entry);

 g_signal_connect(GTK_OBJECT(dialog), "response",

  GTK_SIGNAL_FUNC(addcd_dialog_button_clicked), NULL);

 return dialog;

}

13. База данных требует регистрации пользователя перед выполнением запросов к ней, поэтому данная функция создает диалоговое окно для ввода имени пользователя и пароля:

GtkWidget *create_login_dialog() {

 GtkWidget* dialog = gtk_dialog_new_with_buttons("Database Login",

  app, GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,

  GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, NULL);

 username_entry = gtk_entry_new();

 password_entry = gtk_entry_new();

 gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE);

 add_widget_with_label(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),

  "Username", username_entry);

 add_widget_with_label(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),

  "Password", password_entry);

 gtk_widget_show_all(GTK_WIDGET(GTK_DIALOG(dialog)->vbox));

 return dialog;

}

Упражнение 16.13. callbacks.c

Файл callbacks.с содержит функции, задающие обратные вызовы для виджетов пользовательского интерфейса.

1. Сначала необходимо включить заголовочный файл и ссылки на некоторые определенные в файле interface.c глобальные переменные для чтения и изменения конкретных свойств виджетов:

#include "app_gnome.h"

extern GtkWidget *treeview;

extern GtkWidget *app;

extern GtkWidget *appbar;

extern GtkWidget *artist_entry;

extern GtkWidget *title_entry;

extern GtkWidget *catalogue_entry;

static GtkWidget *addcd_dialog;

2. В функции quit_app вы вызываете функцию database_end для чистки и закрытия базы данных перед выходом:

void quit_app(GtkWidget* window, gpointer data) {

 database_end();

 gtk_main_quit();

}

3. Следующая функция выводит простое диалоговое окно для подтверждения вашего желания завершить приложение, возвращая отклик в виде значения gboolean:

gboolean confirm_exit() {

 gint result;

 GtkWidget* dialog = gtk_message_dialog_new(NULL,

  GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION,

  GTK_BUTTONS_YES_NO, "Are you sure you want to quit?");

 result = gtk_dialog_run(GTK_DIALOG(dialog));

 gtk_widget_destroy(dialog);

 return (result == GTK_RESPONSE_YES);

}

4. delete_event_handler — функция обратного вызова, которую вы связываете с событием главного окна Gdk delete event. Событие генерируется, когда вы пытаетесь закрыть окно до того (что существенно), как послан сигнал GTK+ уничтожения окна:

gboolean delete_event_handler(GtkWidget* window, GdkEvent *event,

 gpointer data) {

 return !confirm_exit();

}

5. Следующая функция вызывается, когда мышью щелкается кнопка в диалоговом окне вставки компакт-диска. Если вы щелкнули мышью кнопку OK, программа копирует строки в массив типа char и передает его данные в интерфейсную функцию MySQL add_cd:

void addcd_dialog_button_clicked(GtkDialog * dialog, gint response,

 gpointer userdata) {

 const gchar *artist_const;

 const gchar* title_const;

 const gchar *catalogue_const;

 gchar artist[200];

 gchar title[200];

 gchar catalogue[200];

 gint *cd_id;

 if (response == GTK_RESPONSE_ACCEPT) {

  artist_const = gtk_entry_get_text(GTK_ENTRY(artist_entry));

  title_const = gtk_entry_get_text(GTK_ENTRY(title_entry));

  catalogue_const = gtk_entry_get_text(GTK_ENTRY(catalogue_entry));

  strcpy(artist, artist_const);

  strcpy(title, title_const);

  strcpy(catalogue, catalogue_const);

  add_cd(artist, title, catalogue, cd_id);

 }

 addcd_dialog = NULL;

 gtk_widget_destroy(GTK_WIDGET(dialog));

}

6. Далее идет самая важная часть приложения: извлечение результатов поиска и заполнение объекта GtkTreeView:

void on_search_button_clicked(GtkButton* button, gpointer userdata) {

 struct cd_search_st cd_res;

 struct current_cd_st cd;

 struct current_tracks_st ct;

 gint res1, res2, res3;

 gchar track_title[110];

 const gchar *search_string_const;

 gchar search string[200];

 gchar search_text[200];

 gint i = 0, j = 0;

 GtkTreeStore *tree_store;

 GtkTreeIter parent_iter, child_iter;

 memset(&track_title, 0, sizeof(track_title));

7. Здесь вы получаете строку поиска из виджета ввода, копируете ее в переменную и выбираете соответствующие ID компакт-дисков:

 search_string_const = gtk_entry_get_text(GTK_ENTRY(userdata));

 strcpy(search_string, search_string_const);

 resl = find_cds(search_string, &cd_res);

8. Затем вы обновляете appbar для вывода сообщения, информирующего пользователя о результатах поиска:

 sprintf(search_text, "Displaying %d result(s) for search string ' %s'",

  MIN(res1, MAX_CD_RESULT), search_string);

 gnome_appbar_push(GNOME_APPBAR(appbar), search_text);

9. Теперь у вас есть результаты поиска, и можно заполнять ими модель GtkTreeStore. Для каждого ID компакт-диска необходимо извлечь соответствующую структуру типа current_cd_st, которая содержит название и исполнителя CD, и затем извлечь список дорожек диска. В заголовочном файле app_mysql.h задано ограничение количества элементов, MAX_CD_RESULT, для того, чтобы не было переполнения модели GtkTreeStore:

 tree_store = gtk_tree_store_new(N_COLUMNS,

  G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);

 while (i < res1 && i < MAX_CD_RESULT) {

  res2 = get_cd(cd_res.cd_id[i], &cd);

  /* В модель вставляется новая строка */

  gtk_tree_store_append(tree_store, &parent_iter, NULL);

  gtk_tree_store_set(tree_store, &parent_iter, COLUMN_TITLE,

   cd.title, COLUMN_ARTIST, cd.artist_name, COLUMN_CATALOGUE,

   cd.catalogue, -1);

  res3 = get_cd_tracks(cd_res.cd_id[i++], &ct);

  j = 0;

  /* Заполнение дерева дорожками текущего компакт-диска */

  while (j < res3) {

   sprintf(track_title, " Track %d. ", j+1);

   strcat(track_title, ct.track[j++]);

   gtk_tree_store_append(tree_store, &child_iter, &parent_iter);

   gtk_tree_store_set(tree_store, &child_iter,

    COLUMN_TITLE, track_title, -1);

  }

 }

 gtk_tree_view_set_model(GTK_TREE_VIEW(treeview),

 GTK_TREE_MODEL(tree_store));

}

10. Диалоговое окно addcd немодальное. Следовательно, перед его созданием и отображением вы проверяете, не активно ли оно уже:

void on_addcd_activate(GtkMenuItem* menuitem, gpointer user_data) {

 if (addcd_dialog != NULL) return;

 addcd_dialog = create_addcd_dialog();

 gtk_widget_show_all(addcd_dialog);

}

gboolean close_app(GtkWidget * window, gpointer data) {

 gboolean exit;

 if ((exit = confirm_exit())) {

  quit_app(NULL, NULL);

 }

 return exit;

}

11. Когда вы щелкаете мышью кнопку About (О программе), раскрывается стандартное поле about среды GNOME:

void on_about_activate(GtkMenuItem* menuitem, gpointer user_data) {

 const char* authors[] = {"Wrox Press", NULL};

 GtkWidget* about = gnome_about_new("CD Database", "1.0",

  " (c) Wrox Press", "Beginning Linux Programming",

  (const char **)authors, NULL, "Translators", NULL);

 gtk_widget_show(about);

}

Упражнение 16.14. Файл main.c

Введите следующий программный код в файл main.с, содержащий функцию main программы.

1. После операторов include вы ссылаетесь на поля ввода имени пользователя и пароля из файла interface.c:

#include <stdio.h>

#include <stdlib.h>

#include "app_gnome.h"

extern GtkWidget* username_entry;

extern GtkWidget* password_entry;

gint main(gint argc, gchar *argv[]) {

 GtkWidget *main_window;

 GtkWidget *login_dialog;

 const char *user_const;

 const char *pass_const;

 gchar username[100];

 gchar password[100];

 gint result;

2. Инициализируйте как обычно библиотеки GNOME и затем создайте и отобразите на экране главное окно и диалоговое окно вашей регистрации:

 gnome_program_init("CdDatabase", "0.1", LIBGNOMEUI_MODULE, argc, argv,

  GNOME_PARAM_APP_DATADIR, "", NULL);

 main_window = create_main_window();

 gtk_widget_show_all(main_window);

 login_dialog = create_login_dialog();

3. Вы ждете в цикле, пока пользователь не введет корректную комбинацию имени пользователя и пароля. Он может выйти из приложения, щелкнув мышью кнопку Cancel, причем в этом случае ему придется подтвердить свое действие:

 while (1) {

  result = gtk_dialog_run(GTK_DIALOG(login_dialog));

  if (result != GTK_RESPONSE_ACCEPT) {

   if (confirm_exit()) return 0;

   else continue;

  }

  user_const = gtk_entry_get_text(GTK_ENTRY(username_entry));

  pass_const = gtk_entry_get_text(GTK_ENTRY(password_entry));

  strcpy(username, user_const);

  strcpy(password, pass_const);

  if (database_start(username, password) == TRUE) break;

4. Если функция database_start завершается аварийно, вы выводите сообщение и диалоговое окно регистрации снова отображается на экране:

  GtkWidget* error_dialog =

   gtk_message_dialog_new(GTK_WINDOW(main_window),

    GTK_DIALOG_DESTROY_WITH_PARENT,

    GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,

    "Could not log on! — Check Username and Password");

  gtk_dialog_run(GTK_DIALOG(error_dialog));

  gtk_widget_destroy(error_dialog);

 }

 gtk_widget_destroy(login_dialog);

 gtk_main();

 return 0;

}

5. Для компиляции этого приложения напишите make-файл. Как и в главе 8, вам, возможно, придется указать место расположения библиотеки mysql-клиента с помощью строки, подобной приведенной далее:

-L/usr/lib/mysql

После опции -L поместите каталог, в котором ваша система хранит библиотеки MySQL:

all: app

app: app_mysql.c callbacks.с interface.c main.с app_gnome.h app_mysql.h

 gcc -o app -I/usr/include/mysql app_mysql.с callbacks.с interface.c main.с -lmysqlclient `pkg-config --cflags --libs libgnome-2.0 libgnomeui-2.0`

clean:

 rm -f app

6. Теперь для компиляции приложения для работы с компакт-дисками просто воспользуйтесь командой make:

make -f Makefile

Когда вы запустите приложение арр, то получите ваше приложение для работы с базой данных компакт-дисков в стиле GNOME (рис. 16.15)!

Рис. 16.15 

Резюме

В этой главе вы узнали о программировании с помощью библиотек GTK+/GNOME для создания приложений с профессионально выглядящем интерфейсом GUI. Сначала вы рассмотрели с X Window System и научились применять комплекты инструментальных средств, а затем вкратце познакомились с принципами работы GTK+ под управлением системы объектов и механизма сигналов/обратных вызовов этого комплекта инструментов.

Далее вы перешли к API виджетов GTK+, продемонстрировав их применение на простых и более сложных примерах, приведенных в нескольких листингах программ. Рассмотрев виджет GnomeApp, вы научились легко создавать меню с помощью вспомогательных макросов. В заключение вы узнали, как создавать модальные и немодальные диалоговые окна для взаимодействия с пользователем.

И в конце главы вы создали средствами GNOME/GTK+ интерфейс пользователя для вашей базы данных компакт-дисков, который позволяет регистрироваться в базе данных, искать компакт-диски и пополнять базу данных новыми CD.

В главе 17 вы познакомитесь с комплектом инструментальных средств, конкурирующим с GTK+, и научитесь программировать в среде KDE, применяя комплект Qt.

Глава 17

Программирование в KDE с помощью Qt

В главе 16 вы познакомились с библиотеками GUI графической среды GNOME/GTK+, предназначенными для создания пользовательского графического интерфейса под управлением системы X. Эти библиотеки — лишь половина истории, другой крупный игрок на поле GUI в системе Linux — графическая среда KDE/Qt, и в этой главе мы рассмотрим ее библиотеки и увидим, как они развиваются в условиях конкуренции.

Комплект инструментальных средств Qt написан на языке С++, стандартный язык для написания приложений Qt/KDE, поэтому в данной главе вам придется отойти от обычного языка С и испачкать свои руки С++. Возможно, вы воспользуетесь этой возможностью и освежите свои знания языка С++, вспомнив прежде всего принципы наследования и инкапсуляции, метод переопределения и виртуальные функции.

В этой главе мы обсудим следующие темы:

□ знакомство с комплектом инструментов Qt;

□ установка Qt;

□ практическое применение;

□ механизм "сигнал/слот";

□ виджеты Qt;

□ диалоговые окна;

□ создание меню и панелей инструментов с помощью KDE;

□ разработка средствами KDE/Qt вашего приложения для работы с базой данных компакт-дисков.

Введение в KDE и Qt

KDE (Desktop Environment, K-среда рабочего стола) — графическая среда рабочего стола с открытым программным кодом, основанная на библиотеке графического пользовательского интерфейса Qt. В состав KDE входит множество приложений и утилит, включая полный офисный пакет, Web-обозреватель и даже полнофункциональную IDE (интегрированная среда разработки) для программирования приложений KDE/Qt (KDevelop обсуждалась в главе 9). Профессиональное признание функциональных возможностей развитых приложений KDE пришло, когда компания Apple выбрала Web-обозреватель KDE в качестве ядра основного Web-обозревателя для системы Mac OS X, названного Safari и известного как очень быстрый обозреватель.

Главная страница проекта KDE находится по адресу http://www.kde.org, на ней вы найдете подробные сведения, файлы загрузки среды KDE и ее приложения, документацию, сможете присоединиться к списку адресатов файлов рассылок и получить другую информацию для разработчиков.

Примечание

Во время написания книги последней версией KDE была версия 3.5.7, и поскольку эта версия включена в современные дистрибутивы Linux, мы считаем, что у вас установлена версия KDE 3.5 или более свежая. Продолжается работа над крупным обновлением — KDE 4.0. Вы можете также загрузить из Интернета предварительные версии KDE 4.0. Точно так же самая свежая версия Qt — 4.3, но в большинстве дистрибутивов Linux установлена версия Qt 3, например версия 3.3, как стандартная версия Qt. В этой главе обсуждается Qt 3.3, потому что она встречается чаще других.

С точки зрения программиста, KDE предлагает десятки виджетов KDE, обычно унаследованных от их аналогов Qt, но улучшенных и облегчающих использование. Виджеты KDE обеспечивают более тесную связь с рабочим столом KDE, чем сам по себе комплект инструментов Qt; у вас появляется, например, возможность управления сеансами.

Qt — тщательно продуманный межплатформный комплект инструментов GUI, написанный на языке С++. Это детище норвежской компании Trolltech, разрабатывающей, продающей и осуществляющей техническую поддержку Qt и сопутствующего программного обеспечения для промышленного рынка. Trolltech во всеуслышание рекламирует межплатформные возможности комплекта Qt, которые бесспорно впечатляющи, Qt поддерживается в ОС Linux и модификациях UNIX, Windows, Mac OS X и даже на встроенных платформах, явно отдающих предпочтение комплекту Qt по сравнению с его конкурентами.

Примечание

Специализированная версия Qt выполняется на сотовых телефонах. Еще одна версия работает на PDA (Personal Digital Assistant, электронный секретарь) Sharp Zaurus и аналогичных платформах. Qt Jambi представляет собой версию комплекта инструментов для языка Java.

В настоящее время компания Trolltech продает коммерческие версии Qt случайным пользователям и любителям по завышенным ценам. К счастью, Trolltech понимает важность бесплатной версии для сообщества, распространяющего свободное программное обеспечение, и предлагает свободно распространяемую версию Qt Open Source Edition для. ОС Linux, Windows и Mac OS X. В ответ Trolltech получает большую базу пользовательских установок, обширное сообщество программистов и высокое реноме своего продукта.

Qt Open Source Edition распространяется на условиях лицензии GPL, т.е. вы можете программировать, используя библиотеки-Qt, и распространять бесплатно собственное программное обеспечение, отвечающее требованиям лицензии GPL. Насколько мы можем судить, у свободно распространяемой версии есть два основных отличия от коммерческих версий: отсутствие технической поддержки и запрет на применение программного обеспечения Qt в коммерческих приложениях. Вся необходимая вам документация по API есть на Web-сайте Trolltech по адресу http://www.trolltech.com.

Установка Qt

Если у вас нет особых причин для компиляции из исходного программного кода, самый простой путь — найти для вашего дистрибутива двоичный пакет или пакет RPM. Дистрибутив Fedora Linux 7 поставляется с пакетом qt-3.3.8-4.i386.rpm, который можно установить с помощью следующей команды.

$ rpm -Uvh qt-3.3.3-4.i386.rpm

Комплект Qt и библиотеки программирования KDE можно также установить с помощью приложения Package Manager (Диспетчер пакетов) — рис. 17.1.

Рис. 17.1 

Если вы хотите загрузить из Интернета исходный программный код и сформировать Qt самостоятельно, самый свежий программный код можно получить с FTP-сайта Trolltech по адресу ftp://ftp.trolltech.com/qt/source/. Пакет исходного программного кода приходит с подробнейшими инструкциями, касающимися компиляции и установки Qt и хранящимися в файле INSTALL, упакованном программой tar.

$ сd /usr/local

$ tar -xvzf qt-x11-free-3.3.8.tar.gz

$ ./configure

$ make

Также следует добавить в файл /etc/ld.so.conf следующую строку:

/usr/lib/qt-3.3/lib

Вставить ее можно в любое место файла.

Примечание

В системах Linux Fedora и Red Hat эту строку нужно сохранить в файле /etc/ld.so.conf.d/qt-i386.conf. Если вы устанавливали Qt, как показано на рис. 17.1, этот этап уже будет пройден.

Если комплект Qt установлен корректно, переменная окружения QTDIR будет содержать каталог установки. Проверить это можно следующим образом:

$ echo $QTDIR

/usr/lib/qt-3.3

Убедитесь также в том, что каталог lib добавлен в файл /etc/ld.so.conf. Затем выполните как суперпользователь следующую команду:

# ldconfig

Испытайте простейшую программу с применением Qt и убедитесь в том, что ваша установка функционирует должным образом (упражнение 17.1).

Упражнение 17.1. Окно QMain

Введите (или скопируйте и вставьте программный код из загруженного файла) приведенную далее программу и назовите ее qt1.cpp:

#include <qapplication.h>

#include <qmainwindow.h>

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 QMainWindow* window = new QMainWindow();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

При компиляции вам необходимо указать Qt-каталоги include и lib:

$ g++ -о qt1 qt1.cpp -I$QTDIR/include -L$QTDIR/lib -lqui

Примечание

На некоторых платформах в конце строки указывается библиотека -lqt. В версии Qt 3.3, тем не менее, используйте -lqui.

Выполнив приложение, вы должны получить окно Qt (рис. 17.2).

$ ./qtl

Рис. 17.2 

Как это работает

В отличие от GTK+ здесь нет вмещающего в себя все заголовочного файла qt.h, поэтому вы должны явно включать заголовочные файлы всех используемых объектов.

Первый объект, с которым вы встречаетесь, — QApplication. Это главный объект Qt, который вы должны сформировать, передав ему в самом начале аргументы командной строки. У каждого приложения Qt должен быть один и только один объект типа QApplication, который вы должны создать перед тем, как делать что-то еще. QApplication имеет дело с внутренними встроенными операциями Qt, такими как обработка событий, размещение строк и управление внешним представлением.

Вы будете применять два метода QApplication: setMainWidget, который создает главный виджет вашего приложения, и exec, который запускает выполнение цикла отслеживания событий. Метод exec не возвращает управление до тех пор, пока либо не будет вызван метод QApplication::quit(), либо не будет закрыт главный виджет.

QMainWindow — базовый виджет окна в Qt, который поддерживает меню, панель инструментов и строку состояния. Он будет играть важную роль в этой главе, по мере того, как вы научитесь расширять его возможности и вставлять в него виджеты, формирующие интерфейс.

Далее мы обсудим механизм программирования, управляемого событиями, и вы вставите в приложение виджет PushButton.

Сигналы и слоты

Как вы видели в главе 16, сигналы и их обработка — главные механизмы, используемые приложениями GUI для реагирования на ввод пользователя, и ключевые функции библиотек GUI. Механизм обработки сигналов комплекта Qt состоит из сигналов и слотов или приемников, называемых сигналами и функциями обратного вызова в комплекте инструментов GTK+ или событиями и обработчиками событий в языке программирования Java.

Примечание

Имейте в виду, что сигналы Qt отличаются от сигналов UNIX, обсуждавшихся в главе 11.

Вот как устроено программирование, управляемое событиями: графический интерфейс пользователя состоит из меню, панелей инструментов, кнопок, полей ввода и множества других элементов GUI, называемых виджетами. Когда пользователь взаимодействует с виджетом, например, активизирует пункт меню или вводит какой-то текст в поле ввода, виджет порождает именованный сигнал, такой как clicked, text_changed или key_pressed. Как правило, вам захочется сделать что-то в ответ на действие пользователя, например, сохранить документ или выйти из приложения, и вы выполняете это, связав сигнал с функцией обратного вызова или слотом на языке Qt.

Применение сигналов и слотов довольно специфично — Qt определяет два новых соответствующим образом описанных псевдоключевых слова, signals и slots для обозначения в вашем программном коде классов сигналов и слотов. Это замечательно с точки зрения читаемости и сопровождения программного кода, но вы вынуждены пропускать свой код через отдельный этап препроцессорной обработки для поиска и замены этих псевдоключевых слов дополнительным кодом на языке С++.

Примечание

Таким образом, программный код с использованием Qt — не настоящий программный код на С++. Порой это становится проблемой для некоторых разработчиков. См. документацию Qt на Web-сайте http://doc.trolltech.com/, чтобы понять причину применения этих новых псевдоключевых слов в С++. Более того, применение сигналов и слотов не так уж отличается от Microsoft Foundation Classes (MFC, библиотека базовых классов Microsoft) в ОС Windows, в которой также используется модифицированное определение языка С++.

На способы применения сигналов и слотов в Qt есть несколько ограничений, но они не слишком существенные:

□ сигналы и слоты должны быть функциями-методами класса-потомка QObject;

□ при использовании множественного наследования QObject должен быть первым в списке класса;

□ оператор Q_OBJECT должен появляться первым в объявлении класса;

□ сигналы нельзя применять в шаблонах;

□ указатели на функцию не могут использоваться как аргументы в сигналах и слотах;

□ сигналы и слоты не могут переопределяться или обновляться до статуса public (общедоступный).

Поскольку вы должны писать ваши сигналы и слоты как потомков объекта QObject, логично создавать ваш интерфейс, расширяя и настраивая виджет, начиная с QWidget, базового виджета Qt, потомка виджета QObject. В комплекте Qt вы почти всегда будете создавать интерфейсы, расширяя такие виджеты, как QMainWindow.

Типичное определение класса в файле MyWindow.h для вашего GUI будет напоминать приведенное далее:

class MyWindow : public QMainWindow {

 Q_OBJECT

public:

 MyWindow();

 virtual ~MyWindow();

signals:

 void aSignal();

private slots:

 void doSomething();

}

Ваш класс — наследник объекта QMainWindow, который определяет функциональные возможности главного окна в приложении. Аналогичным образом при создании диалогового окна вы определите подкласс QDialog. Первым указан оператор Q_OBJECT, действующий как метка для препроцессора, за которым следуют обычные объявления конструктора и деструктора. Далее даны определения сигнала и слота.

У вас есть один сигнал и один слот, оба без параметров. Для порождения сигнала aSignal() вам нужно всего лишь в любом месте программы вызвать функцию emit:

emit aSignal();

Это означает, что все остальное обрабатывается Qt. Вам даже не потребуется реализация aSignal().

Для применения слотов их нужно связать с сигналом. Делается это соответствующим образом с помощью названного статического метода connect класса QObject:

bool QObject::connect(const QObject * sender, const char* signal,

 const QObject * receiver, const char * member);

Просто передайте объект, владеющий сигналом (отправитель), функцию сигнала, объект, владеющий слотом (приемником), и в завершение укажите имя слота.

В примере MyWindow, если бы вы захотели связать сигнал clicked виджета QPushButton с вашим слотом doSomething, вы бы написали:

connect(button, SIGNAL(clicked()), this, SLOT(doSomething()));

Учтите, что необходимо применять макросы SIGNAL и SLOT для выделения функций сигналов и слотов. Как и в комплекте GTK+, вы можете связать ряд слотов с заданным сигналом и также связать слот с любым количеством сигналов с помощью множественных вызовов функции connect. Если она завершается аварийно, то возвращает FALSE.

Остается реализовать ваш слот в виде обычной функции-метода:

void MyWindow::doSomething() {

 // Код слота

}

Выполните упражнение 17.2.

Упражнение 17.2. Сигналы и слоты

Теперь, зная основы использования сигналов и слотов, применим их в примере. Усовершенствуйте QMainWindow, вставьте в него кнопку и свяжите сигнал кнопки clicked со слотом.

1. Введите следующее объявление класса и назовите файл ButtonWindow.h:

#include <qmainwindow.h>

class ButtonWindow : public QMainWindow {

 Q_OBJECT

public:

 ButtonWindow(QWidget *parent = 0, const char *name = 0);

 virtual ~ButtonWindow();

private slots:

 void Clicked();

};

2. Далее следует реализация класса в файле ButtonWindow.cpp:

#include "ButtonWindow.moc"

#include <qpushbutton.h>

#include <qapplication.h>

#include <iostream>

3. В конструкторе вы задаете заголовок окна, создаете кнопку и связываете сигнал нажатия кнопки с вашим слотом. setCaption — метод объектов типа QMainWindow, который, что неудивительно, задает заголовок окна:

ButtonWindow::ButtonWindow(QWidget *parent, const char* name) : QMainWindow(parent, name) {

 this->setCaption("This is the window Title");

 QPushButton *button = new QPushButton("Click Me!", this, "Button1");

 button->setGeometry(50, 30, 70, 20);

 connect(button, SIGNAL(clicked()), this, SLOT(Clicked()));

}

4. Qt автоматически удаляет виджеты, поэтому ваш деструктор пуст:

ButtonWindow::~ButtonWindow() {}

5. Затем реализация слота:

void ButtonWindow::Clicked(void) {

 std::cout << "clicked!\n";

}

6. И наконец, в функции main вы просто создаете экземпляр типа ButtonWindow, делаете его главным окном вашего приложения и отображаете окно на экране:

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 ButtonWindow *window = new ButtonWindow();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

7. Прежде чем вы сможете откомпилировать данный пример, необходимо запустить препроцессор для заголовочного файла. Программа этого препроцессора называется Meta Object Compiler (moc, компилятор метаобъекта) и должна быть включена в пакет комплекта Qt. Выполните moc для файла ButtonWindow.h, сохранив результат в файле ButtonWindow.moc:

$ moc ButtonWindow.h -о ButtonWindow.moc

Теперь можно компилировать как обычно, скомпоновав с результатом команды moc.

$ g++ -о button ButtonWindow.срр -I$QTDIR/include -L$QTDIR/lib -lqui

Выполнив программу, вы получите пример, показанный на рис. 17.3.

Рис. 17.3 

Как это работает

В этом примере мы ввели новый виджет и некоторые новые функции, поэтому давайте их рассмотрим. QPushButton — виджет простой кнопки, хранящий метку и растровую графику и способный активизироваться при щелчке пользователя кнопкой мыши или при нажатии клавиш.

Конструктор объекта QPushButton очень прост.

QPushButton::QPushButton(const QString &text, QWidget *parent,

 const char* name=0);

Первый аргумент — текст метки кнопки, далее родительский виджет и последний аргумент — имя кнопки, обычно применяемое Qt для внутренних операций.

Параметр родительского виджета, общий для всех объектов, — QWidget, он управляет отображением и уничтожением и разными другими свойствами. Передача NULL в качестве родительского объекта означает виджет верхнего уровня, при этом создается содержащее его пустое окно. В примере вы передаете текущий объект ButtonWindow с помощью ключевого слова this, что приводит к вставке кнопки в основную область окна ButtonWindow.

Аргумент name задает имя виджета для внутреннего использования Qt. Если комплект Qt обнаружит ошибку, имя виджета будет выведено в сообщении об ошибке, поэтому неплохо выбирать подходящие имена виджетов, поскольку при отладке это сбережет массу времени.

Вы могли заметить, что объект QPushButton очень примитивно вставляется в окно ButtonWindow, с помощью параметра parent конструктора QPushButton, без указания положения кнопки, ее размера, рамки или чего-либо еще. Если вы хотите управлять внешним видом кнопки, что очень важно для создания привлекательного интерфейса, следует применять виджеты компоновки комплекта Qt. Давайте их сейчас рассмотрим,

В Qt есть целый ряд способов размещения и компоновки виджетов. Вы уже видели использование абсолютных координат с помощью вызова setGeometry, но они редко применяются, поскольку виджеты не масштабируются и не меняют размеры при изменении величины окна.

Предпочтительный метод компоновки виджетов — применение классов QLayout или виджетов-контейнеров, которые изменяют свои размеры соответствующим образом после задания им подсказок, касающихся отступов и расстояний между виджетами.

Ключевое различие между классами QLayout и упаковочными контейнерами заключается в том, что объекты класса QLayout не являются виджетами.

Классы компоновки — потомки объектов, типа QObject, а не QWidget, поэтому их применение ограничено. Например, вы не можете создать объект QVBoxLayout — основной виджет объекта QMainWindow.

Виджеты упаковочных контейнеров (такие, как QHBox и QVBox) напротив — потомки объекта типа QWidget следовательно, вы можете применять их как обычные виджеты. Возможно, вас удивляет, что в Qt есть и классы QLayout, и виджеты QBox с дублирующимися функциональными возможностями. На самом деле виджеты QBox существуют только для удобства и по существу служат оболочкой классов QLayout в типе QWidget. Объекты QLayout обладают возможностью автоматического изменения размеров, в то время как размеры виджетов нужно изменять вручную с помощью вызова метода QWidget::resizeEvent().

Подклассы QLayout: QVBoxLayout и QHBoxLayout, — самый распространенный способ создания интерфейса, и именно их вы будете чаще всего встречать в программном коде с применением Qt.

QVBoxLayout и QHBoxLayout — невидимые объекты-контейнеры, хранящие другие виджеты и схемы размещения с вертикальной и горизонтальной ориентациями соответственно. Вы сможете создавать сколь угодно сложные компоновки виджетов, поскольку допускается использование вложенных компоновок, например, за счет вставки как элемента горизонтальной схемы размещения внутрь вертикального упаковочного контейнера.

Есть три конструктора QVBoxLayout, заслуживающих внимания (у объектов QHBoxLayout идентичный API).

QVBoxLayout::QVBoxLayout(QWidget *parent, int margin, int spacing,

 const char *name)

QVBoxLayout::QVBoxLayout(QLayout *parentLayout, int spacing,

 const char * name)

QVBoxLayout::QVBoxLayout(int spacing, const char *name)

Родителем объекта QLayout может быть либо виджет, либо другой объект типа QLayout. Если не задавать родительский объект, вы сможете только вставить позже данную схему размещения в другой объект QLayout с помощью метода addLayout.

Параметры margin и spacing задают пустое пространство в пикселах вокруг схемы размещения QLayout и между отдельными виджетами в ней.

После создания вашей схемы размещения QLayout вы можете вставлять дочерние виджеты или схемы с помощью следующей пары методов:

QBoxLayout::addWidget(QWidget *widget, int stretch = 0, int alignment = 0);

QBoxLayout::addLayout(QLayout *layout, int stretch = 0);

Выполните упражнение 17.3.

Упражнение 17.3. Применение классов QBoxLayout

В этом примере вы увидите в действии классы QBoxLayout при размещении виджетов QLabel в окне QMainWindow.

1. Сначала введите заголовочный файл LayoutWindow.h:

#include <qmainwindow.h>

class LayoutWindow : public QMainWindow {

 QOBJECT

public:

 LayoutWindow(QWidget *parent = 0, const char *name = 0);

virtual ~LayoutWindow();

};

2. Теперь введите реализацию в файл LayoutWindow.cpp:

#include <qapplication.h>

#include <qlabel.h>

#include <qlayout.h>

#include "LayoutWindow.moc"

LayoutWindow::LayoutWindow(QWidget* parent, const char *name) :

 QMainWindow(parent, name) {

 this->setCaption("Layouts");

3. Необходимо создать фиктивный QWidget для хранения объекта QHBoxLayout, поскольку его нельзя напрямую вставить в объект QMainWindow:

 QWidget *widget = new QWidget(this);

 setCentralWidget(widget);

 QHBoxLayout *horizontal = new QHBoxLayout(widget, 5, 10, "horizontal");

 QVBoxLayout *vertical = new QVBoxLayout();

 QLabel* label1 = new QLabel("Top", widget, "textLabel1");

 QLabel* label2 = new QLabel("Bottom", widget, "textLabel2");

 QLabel* label3 = new QLabel("Right", widget, "textLabel3");

 vertical->addwidget(label1);

 vertical->addwidget(label2);

 horizontal->addLayout(vertical);

 horizontal->addWidget(label3);

 resize(150, 100);

}

LayoutWindow::~LayoutWindow() { }

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 LayoutWindow *window = new LayoutWindow();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

Как и прежде, перед компиляцией нужно выполнить moc для заголовочного файла:

$ moc LayoutWindow.h -о LayoutWindow.moc

$ g++ -о layout LayoutWindow.cpp -I$QTDIR/include -L$QTDIR/lib -lqui

Выполнив эту программу, вы получите схему размещения ваших меток QLabel (рис. 17.4). Попробуйте изменить величину окна и посмотрите, как расширяются и сжимаются метки, заполняя все доступное пространство.

Рис. 17.4

Как это работает

Программа LayoutWindow.cpp создает два виджета упаковочных контейнеров, горизонтальный и вертикальный контейнер для размещения виджетов. Вертикальный контейнер получает две метки, описанные, соответственно, как Top и Bottom. Горизонтальный контейнер также содержит два виджета, метку, обозначенную Right, и вертикальный контейнер. Вы можете помещать компоновочные виджеты внутрь других компоновочных виджетов, как показано в данном примере.

Попробуйте изменить исходный текст программы в файле LayoutWindow.срр, чтобы поэкспериментировать и лучше понять, как работают компоновочные виджеты.

Мы рассмотрели основы применения Qt — сигналы и слоты, команду moc и средства компоновки. Теперь пора более внимательно изучить виджеты.

Виджеты Qt

Для каждого случая в Qt есть виджеты, и их подробное рассмотрение займет всю оставшуюся часть книги. В этом разделе мы познакомимся с виджетами Qt общего применения, включая виджеты для ввода данных, кнопки, обычные и раскрывающиеся списки.

QLineEdit

QLineEdit — виджет для ввода однострочного текста. Применяйте его для ввода коротких фрагментов текста, таких как имя пользователя. В виджете QLineEdit можно ограничить длину вводимого текста с помощью маски ввода, предлагающей заполнить шаблон, или для дополнительного контроля можно применить функцию проверки допустимости, например, чтобы убедиться в том, что пользователь вводит корректные дату, номер телефона или подобные величины. У виджета QLineEdit есть функции редактирования, позволяющие выбирать части текста, вырезать и вставлять, отменять и повторять изменения, как командами пользователя, так и средствами API.

Далее перечислены конструкторы и наиболее полезные методы.

#include <qlineedit.h>

QLineEdit::QLineEdit(QWidget *parent, const char* name = 0);

QLineEdit::QLineEdit(const QString& contents, QWidget *parent,

 const char *name = 0);

QLineEdit::QLineEdit(const QString& contents, const QString& inputMask,

 QWidget *parent, const char* name = 0);

void setInputMask(const QString& inputMask);

void insert(const QString& newText);

bool isModified(void);

void setMaxLength(int length);

void setReadOnly(bool read);

void setText(const QString &text);

QString text(void);

void setEchoMode(EchoMode mode);

В конструкторах вы задаете как обычно родительский виджет и имя виджета с помощью параметров parent и name.

Интересно свойство EchoMode, определяющее способ отображения текста в виджете. Оно может принимать одно из трех значений:

QLineEdit::Normal — отображать вводимые символы (по умолчанию);

□ QLineEdit::Password — отображать звездочки на месте символов;

□ QLineEdit::NoEcho — ничего не отображать. Задается режим отображения с помощью метода setEchoMode:

lineEdit->setEchoMode(QLineEdit::Password);

Усовершенствование, внесенное в версию Qt 3.2, — свойство inputMask, ограничивающее ввод в соответствии с правилом маски.

inputMask — это строка, сформированная из символов, каждый из которых соответствует правилу, принимающему диапазон определенных символов. Если вы знакомы с регулярными выражениями, inputMask использует во многом тот же самый принцип.

Есть два сорта символов, формирующих inputMask: первые указывают на необходимость присутствия определенного символа, вторые при наличии символа добиваются его соответствия заданному правилу. В табл. 17.1 приведены примеры таких символов и их значения.

Таблица 17.1

Обязательный символ Символы, которые разрешены, но не обязательны Значение
A a Символы ASCII А–Z, а–z
N n Символы ASCII A–Z, a–z, 0–9
X x Любой символ
9 0 Цифры 0–9
D d Цифры 1–9

Наша inputMask — это строка, сформированная комбинацией этих символов и необязательно завершающаяся точкой с запятой. Существуют дополнительные специальные символы, у которых также есть значения (табл. 17.2).

Таблица 17.2

Символ Значение
# Разрешен, но не обязателен знак +/-
> Преобразует все последующие введенные символы в символы верхнего регистра.
< Преобразует все последующие введенные символы в символы нижнего регистра
! Останавливает преобразование регистра
\ Символ управляющей последовательности для применения специальных символов в качестве разделителей

Все остальные символы в inputMask действуют как разделители в поле ввода QLineEdit.

В табл. 17.3 приведены примеры масок ввода и соответствующий им текст для ввода.

Таблица 17.3

Пример Допустимый ввод
"AAAAAA-999D" Допустимо Athens-2004, но не Sydney-2000 или Atlanta-1996
"ААААnn-99-99;" Допустимо March-03-12, но не Мау-03-12 или September-03-12
"000.000.000.000" Допустим IP-адрес, например, 192.168.0.1

Выполните упражнение 17.4.

Упражнение 17.4. Виджет QLineEdit

Посмотрим, как действует виджет QLineEdit.

1. Сначала — заголовочный файл LineEdit.h:

#include <qmainwindow.h>

#include <qlineedit.h>

#include <qstring.h>

class LineEdit : public QMainWindow {

 Q_OBJECT

public:

 LineEdit(QWidget *parent = 0, const char *name = 0);

 QLineEdit *password_entry;

private slots:

 void Clicked();

};

2. LineEdit.cpp — уже знакомый файл реализации класса:

#include "LineEdit.moc"

#include <qpushbutton.h>

#include <qapplication.h>

#include <qlabel.h>

#include <qlayout.h>

#include <iostream>

LineEdit::LineEdit(QWidget *parent, const char *name) :

 QMainWindow(parent, name) {

 QWidget *widget = new QWidget(this);

 setCentralWidget(widget);

3. Для компоновки виджетов примените QGridLayout. Задайте число строк и столбцов, величины отступов и расстояния между виджетами:

 QGridLayout *grid = new QGridLayout(widget, 3, 2, 10, 10, "grid");

 QLineEdit *username_entry = new QLineEdit(widget, "username_entry");

 password_entry = new QLineEdit(widget, "password_entry");

 password_entry->setEchoMode(QLineEdit::Password);

 grid->addWidget(new QLabel("Username", widget, "userlabel"), 0, 0, 0);

 grid->addwidget(new QLabel("Password", widget, "passwordlabel"), 1, 0, 0);

 grid->addWidget(username_entry, 0, 1, 0);

 grid->addWidget(password_entry, 1, 1, 0);

 QPushButton *button = new QPushButton("Ok", widget, "button");

 grid->addWidget(button, 2, 1, Qt::AlignRight);

 resize(350, 200);

 connect(button, SIGNAL(clicked()), this, SLOT(Clicked()));

}

void LineEdit::Clicked(void) {

 std::cout << password_entry->text() << "\n";

}

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 LineEdit *window = new LineEdit();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

Выполнив эту программу, вы должны получить результат, показанный на рис. 17.5.

Рис. 17.5

Как это работает

Вы создали два виджета QLineEdit, один подготовили для ввода пароля, задав EchoMode, и заставили его выводить содержимое при щелчке мышью кнопки PushButton. Обратите внимание на виджет QGridLayout, который очень полезен для размещения виджетов в табличной сетке. Когда виджет вставляется в сетку таблицы, вы передаете номер строки и столбца, нумерация начинается с 0, нулевые номера строки и столбца у верхней левой ячейки.

Кнопки Qt

Кнопки виджетов вездесущи и мало отличаются внешним видом, способом применения и API в разных комплектах инструментов. Неудивительно, что Qt предлагает стандартные кнопки PushButton, флажки CheckBox и радиокнопки (или зависимые переключатели) RadioButton.

QButton: базовый класс кнопок

Все виджеты кнопок в комплекте Qt — потомки абстрактного класса QButton. У этого класса есть методы для опроса и переключения включенного/выключенного состояния кнопки и задания текста кнопки или ее графического представления.

Вам никогда не придется обрабатывать виджет типа QButton (не путайте с виджетом QPushButton!), поэтому нет смысла приводить конструкторы. Далее перечислено несколько полезных функций-методов этого класса:

#include <qbutton.h>

virtual void QButton::setText(const QString&);

virtual void QButton::setPixmap(const QPixmap&);

bool QButton::isToggleButton() const;

virtual void QButton::setDown(bool);

bool QButton::isDown() const;

bool QButton::isOn() const;

enum QButton::ToggleState { Off, NoChange, On }

ToggleState QButton::state() const;

У функций isDown и isOn одно назначение. Обе они возвращают TRUE, если кнопка была нажата или активизирована.

Часто вам нужно отключить или сделать серым вариант, если он недоступен в данный момент. Сделать недоступным любой виджет, включая QButton, можно с помощью вызова метода QWidget::setEnable(FALSE).

У QButton есть три подкласса, заслуживающие внимания:

□ QPushButton — виджет простой кнопки, выполняющий некоторое действие при щелчке кнопкой мыши;

□ QCheckBox — виджет кнопки, способный изменять состояние с включенного на выключенное для обозначения некоторого выбора;

□ QRadioButton — виджет кнопки, обычно применяемый в группе таких же кнопок, только одна из которых может быть активна в любой момент времени.

QPushButton

QPushButton — стандартная кнопка общего вида, содержащая текст, такой как "OK" или "Cancel" и/или пиксельную пиктограмму. Как все кнопки класса QButton, она порождает при активизации сигнал clicked и обычно используется для связи со слотом и выполнения некоторого действия.

Вы уже применяли кнопку QPushButton в примерах, и есть лишь еще одна интересная деталь, касающаяся этого простейшего из виджетов Qt. Кнопку QPushButton можно превратить из кнопки, не помнящей своего состояния, в кнопку-выключатель (т.е. способную быть включенной и выключенной), вызвав метод setToggleButton. (Если помните, у комплекта GTK+ из предыдущей главы есть для этих целей разные виджеты.)

Далее для полноты описания приведены конструкторы и полезные методы.

#include <qpushbutton.h>

QPushButton(QWidget *parent, const char *name = 0);

QPushButton(const QString& text, QWidget *parent, const char *name = 0);

QPushButton(const QIconSet& icon, const QString& text,

 QWidget *parent, const char * name = 0);

void QPushButton::setToggleButton(bool);

QCheckBox

QCheckBox — это кнопка, у которой есть состояние, ее можно включить и выключить (или установить и сбросить). Внешний вид QCheckBox зависит от стиля отображения окон текущей системы (Motif, Windows и т.д.), но обычно она отображается как флажок с сопроводительным текстом справа.

Вы можете также перевести кнопку QCheckBox в третье промежуточное состояние, которое означает "без изменения". Оно бывает полезно в редких случаях, когда вы не можете прочесть состояние выбора, который предоставляет кнопка QCheckBox (и, следовательно, самостоятельно установить или сбросить флажок), но хотите дать пользователю возможность оставить выбор неизменным наряду с установкой и сбросом.

#include <qcheckbox.h>

QCheckBox(QWidget *parent, const char *name = 0);

QCheckBox(const QString& text, QWidget *parent, const char *name = 0);

bool QCheckBox::isChecked();

void QCheckBox::setTristate(bool y = TRUE);

bool QCheckBox::isTristate();

QRadioButton

Радиокнопки — кнопки-переключатели, применяемые для отображения исключающего выбора, когда можно выбрать только один вариант из группы представленных (вспомните снова старые автомобильные радиоприемники, в которых можно было нажать только одну кнопку блока). Сами по себе кнопки QRadioButton не многим отличаются от кнопок QCheckBox, поскольку группировка и исключительный выбор обрабатываются классом QButtonGroup, главное же их отличие заключается в том, что они отображаются как круглые кнопки, а не как флажки.

QButtonGroup — виджет, облегчающий обработку групп кнопок за счет предоставления удобных методов.

#include <qbuttongroup.h>

QButtonGroup(QWidget *parent = 0, const char* name = 0);

QButtonGroup(const QString& title, QWidget* parent = 0,

 const char * name = 0);

int insert (QButton *button, int id = -1);

void remove(QButton *button);

int id(QButton *button) const;

int count() const;

int selectedId() const;

Применять виджет QButtonGroup проще простого: он даже предлагает необязательную рамку вокруг кнопок, если используется конструктор title.

Добавить кнопку в QButtonGroup можно с помощью метода insert или заданием QButtonGroup в качестве родительского виджета кнопки. Для уникального обозначения каждой кнопки в группе можно задать id в методе insert. Это особенно полезно при определении выбранной кнопки, т.к. функция selectedId возвращает id выбранной кнопки.

Все кнопки QRadioButton, добавляемые в группу, автоматически становятся кнопками с исключающим выбором.

Далее приведены прототипы конструкторов QRadioButton и одного уникального метода, который не вызовет большого удивления:

#include <qradiobutton.h>

QRadioButton(QWidget* parent, const char* name = 0);

QRadioButton(const QString& text, QWidget *parent, const char *name = 0);

bool QRadioButton::isChecked();

Выполните упражнение 17.5.

Упражнение 17.5. Виджет QButton

Теперь применим полученные знания в примере с кнопками Qt. Приведенная далее программа создает кнопки разных типов (радиокнопки, флажки и простые кнопки), чтобы показать, как использовать эти виджеты в ваших приложениях.

1. Введите файл Buttons.h:

#include <qmainwindow.h>

#include <qcheckbox.h>

#include <qbutton.h>

#include <qradiobutton.h>

class Buttons : public CMainWindow {

 Q_OBJECT

public:

 Buttons(QWidget *parent = 0, const char *name = 0);

2. Вы запросите состояние ваших кнопок позже, в функции слота, поэтому объявите указатели кнопок и вспомогательную функцию PrintActive с атрибутом private в объявлении класса:

private:

 void PrintActive(QButton *button);

 QCheckBox *checkbox;

 QRadioButton *radiobutton1, *radiobutton2;

private slots:

 void Clicked();

}

3. Далее следует файл Buttons.срр:

#include "Buttons.moc"

#include <qbuttongroup.h>

#include <qpushbutton.h>

#include <qapplication.h>

#include <qlabel.h>

#include <qlayout.h>

#include <iostream>

Buttons::Buttons(QWidget *parent, const char *name) :

 QMainWindow(parent, name) {

 QWidget* widget = new QWidget(this);

 setCentralWidget(widget);

 QVBoxLayout *vbox = new QVBoxLayout(widget, 5, 10, "vbox");

 checkbox = new QCheckBox("CheckButton", widget, "check");

 vbox->addWidget(checkbox);

4. Затем вы создаете QButtonGroup для двух ваших радиокнопок (переключателей).

 QButtonGroup *buttongroup = new QButtonGroup(0);

 radiobutton1 = new QRadioButton("RadioButton1", widget, "radio1");

 buttongroup->insert(radiobutton1);

 vbox->addWidget(radiobutton1);

 radiobutton2 = new QRadioButton("RadioButton2", widget, "radio2");

 buttongroup->insert(radiobutton2);

 vbox->addWidget(radiobutton2);

 QPushButton* button = new QPushButton("Ok", widget, "button");

 vbox->addWidget(button);

 resize(350, 200);

 connect(button, SIGNAL(clicked()), this, SLOT(Clicked()));

}

5. Затем приведен удобный метод для вывода состояния заданной кнопки QButton:

void Buttons::PrintActive(QButton *button) {

 if (button->isOn())

  std::cout << button->name() << " is checked\n";

 else

  std::cout" << button->name() << " is not checked\n";

}

void Buttons::Clicked(void) {

 PrintActive(checkbox);

 PrintActive(radiobutton1);

 PrintActive(radiobutton2);

 std::cout << "\n";

}

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 Buttons *window = new Buttons();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

Как это работает

Этот простой пример показывает, как опрашивать виджеты кнопок Qt разных типов. После создания все они по большей части действуют одинаково. Например, функция PrintActive демонстрирует, как получить состояние кнопки (включена или выключена). Обратите внимание на то, как она действует в случае запоминающих состояние кнопок разных типов, таких как флажки и переключатели (радиокнопки). В основном отличаются только вызовы для создания виджета кнопки. Радиокнопки, наиболее сложные (т.к. только одна в группе может быть включена), при создании требуют больше всего работы. В случае радиокнопок вы должны создать QButtonGroup для того, чтобы гарантировать активность только одной радиокнопки в группе в любой момент времени.

QComboBox

Переключатели (радиокнопки) — отличный способ, позволяющий пользователю выбрать из небольшого числа вариантов, скажем шести или меньше. Если вариантов больше шести, ситуация начинает выходить из-под контроля и становится еще более напряженной, когда количество вариантов растет, что приводит к ощутимому увеличению размера окна. В этом случае прекрасным решением может быть использование поля ввода с раскрывающимся меню, также называемое раскрывающимся списком (combo box). Варианты выводятся, когда вы щелкаете кнопкой мыши и открываете меню и количество вариантов при этом ограничено только удобством поиска в списке.

В виджете QComboBox сочетаются функциональные возможности виджетов QLineEdit и QPushButton и раскрывающихся меню, позволяя выбрать один вариант из неограниченного набора вариантов.

QComboBox может быть открытым, как для чтения и записи, так и только для чтения. Если он позволяет читать и записывать, пользователь может ввести новый вариант в дополнение к предлагаемым; в противном случае пользователь ограничен выбором варианта из раскрывающегося списка.

При создании виджета QComboBox можно указать, открыт ли он для чтения и записи или только для чтения, задавая логическое значение в конструкторе:

QComboBox *combo = new QComboBox(TRUE, parent, "widgetname");

Передача значения TRUE переводит QComboBox в режим "чтение/запись". Остальные параметры — обычный указатель на родительский виджет и имя создаваемого виджета.

Как все виджеты Qt, QComboBox обладает гибкостью и предлагает широкий набор функциональных возможностей.

Вы можете добавлять варианты по одному или набором, как тип QString или в стандартном формате char*.

Для вставки одного варианта вызовите функцию insertItem:

combo->insertItem(QString("An Item"), 1);

Приведенная функция принимает объект типа QString и номер позиции в списке. В данном случае 1 вставляет вариант в список первым.

Для добавления в конец списка задайте любое отрицательное целое число.

Гораздо чаще вы будете вставлять несколько элементов списка одновременно, для этого можно применить класс QStrList или, как показано далее, массив char*:

char* weather[] = {"Thunder", "Lightning", "Rain", 0};

combo->insertStrList(weather, 3);

И снова вы можете задать номер позиции вставляемых в список элементов.

Если в виджете QComboBox задан режим "чтение/запись", вводимые пользователем варианты могут автоматически вставляться в список. Это очень полезное, экономящее время свойство, избавляющее пользователя от повторного набора варианта, если он хочет уже введенный вариант использовать несколько раз.

Метод InsertionPolicy управляет позицией вводимого в список элемента. Вы можете выбрать одно из значений, приведенных в табл. 17.4.

Таблица 17.4

Значение Действие
QComboBox::AtTop Вставляет вводимый в список элемент первым
QComboBox::AtBottom Вставляет вводимый в список элемент последним
QComboBox::AtCurrent Заменяет предварительно выбранный вариант в списке
QComboBox::BeforeCurrent Вставляет вводимый элемент перед предварительно выбранным вариантом из списка
QComboBox::AfterCurrent Вставляет вводимый элемент после предварительно выбранного варианта из списка
QComboBox::NoInsertion Новый элемент не вставляется в список вариантов

Для задания политики вызовите метод InsertionPolicy виджета QComboBox:

combo->setInsertionPolicy(QComboBox::AtTop);

Давайте бросим взгляд на конструкторы и методы выбора варианта виджета QComboBox:

#include <qcombobox.h>

QComboBox(QWidget *parent = 0, const char *name = 0);

QComboBox(bool readwrite, QWidget *parent = 0, const char *name = 0);

int count();

void insertStringList(const QStringList& list, int index = -1);

void insertStrList(const QStrList& list, int index = -1);

void insertStrList(const QStrList *list, int index = -1);

void insertStrList (const char **strings, int numStrings = -1, int index = -1);

void insertItem(const QString &t, int index = -1);

void removeItem(int index);

virtual void setCurrentItem(int index);

QString currentText();

virtual void setCurrentText(const QString &);

void setEditable(bool);

Функция count возвращает количество вариантов в списке. QStringList и QStrList — классы коллекций, которые можно применять для вставки вариантов. Удалить варианты можно с помощью метода removeItem, извлечь и задать текущий вариант можно, с помощью методов currentText и setCurrentText, а перейти в редактируемый режим — с помощью метода setEditable.

QComboBox порождает сигнал textChanged(QString&) при каждом новом выборе варианта, передавая вновь выбранный элемент как аргумент.

Выполните упражнение 17.6.

Упражнение 17.6. Виджет QComboBox

В этом примере вы сделаете попытку применить виджет QComboBox и посмотрите, как ведут себя сигналы и слоты с параметрами. Вы создадите класс ComboBox, потомка QMainWindow. В нем будут два виджета QComboBox: один для чтения/записи, другой только для чтения. Вы установите связь с сигналом textChanged для того, чтобы получать текущее значение при каждом его изменении.

1. Введите следующий программный код и назовите файл ComboBox.h:

#include <qmainwindow.h>

#include <qcombobox.h>

class ComboBox : public QMainWindow {

 Q_OBJECT

public:

 ComboBox(QWidget* parent = 0, const char *name = 0);

private slots:

 void Changed(const QString& s);

};

2. Интерфейс состоит из двух виджетов QComboBox: один редактируемый, а другой предназначен только для чтения. Вы заполните оба списка одними и теми же вариантами:

#include "ComboBox.moс"

#include <qlayout.h>

#include <iostream>

ComboBox::ComboBox(QWidget *parent, const char *name) :

 QMainWindow(parent, name) {

 QWidget *widget = new QWidget(this);

 setCentralWidget(widget);

 QVBoxLayout *vbox = new QVBoxLayout(widget, 5, 10, "vbox");

 QComboBox *editablecombo = new QComboBox(TRUE, widget, "editable");

 vbox->addWidget(editablecombo);

 QComboBox *readonlycombo = new QComboBox(FALSE, widget, "readonly");

 vbox->addWidget(readonlycombo);

 static const char* items[] = {"Macbeth", "Twelfth Night", "Othello", 0};

 editablecombo->insertStrList(items);

 readonlycombo->insertStrList(items);

 connect(editablecombo, SIGNAL(textchanged(const QString&),

  this, SLOT(Changed(const QString&)));

 resize(350, 200);

}

3. Далее приведена функция слота. Обратите внимание на параметр s типа QString, передаваемый сигналом:

void ComboBox::Changed(const QString& s) {

 std::cout << s << "\n";

}

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 ComboBox* window = new ComboBox();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

Вы сможете видеть вновь выбранные из редактируемого QComboBox варианты в командной строке на рис. 17.6.

Рис. 17.6

Как это работает

Создаются виджеты раскрывающегося списка во многом так же, как и другие виджеты. Главная новая деталь — вызов функции insertStrList для сохранения списка вариантов в виджете.

Как и в других содержащих текст виджетах, можно задать функцию, которая будет вызываться каждый раз при изменении значения или в общем случае текста раскрывающегося списка.

QListView

Списки и деревья в комплекте Qt формируются виджетом QListView. Этот виджет представляет как простые списки, так и иерархические данные, разделенные на строки и столбцы. Он очень подходит для вывода структур каталогов или чего-то подобного, потому что дочерние элементы можно раскрыть и свернуть, щелкнув кнопкой мыши знак "плюс" или "минус", так же как в файловом обозревателе.

В отличие от виджета GTK+ ListView виджет QListView обрабатывает и данные, и их представление, что сделано для облегчения использования, если не для исключительной гибкости.

В виджете QListView можно выбрать строки или отдельные ячейки и затем вырезать и вставить данные, отсортировать их по столбцу и вы получите виджеты QCheckBox, отображенные в ячейках. В этот виджет встроено множество функциональных возможностей — как программисту вам достаточно лишь вставить данные и задать некоторые правила форматирования.

Создается виджет QListView обычным образом, заданием родительского виджета и собственного имени виджета:

QListView *view = new QListView(parent, "name");

Для задания заголовков столбцов используйте соответствующим образом названный метод addColumn:

view->addColumn("Left Column", width1); // фиксированной ширины

view->addColumn("Right Column"); // с автоматически задаваемым размером

Ширина столбца задается в пикселах или, если пропущена, приравнивается к величине самого длинного элемента в столбце. В дальнейшем при вставке и удалении элементов ширина столбца автоматически меняется.

Данные вставляются в QListView с помощью объекта QListViewItem, представляющего строку данных. Вы должны лишь передать в конструктор объект QListView и элементы строки, и она добавится в конец представления:

QListViewItem *toplevel = new QListViewItem(view, "Left Data", "Right Data");

Первый параметр — либо объект QListView, как в данном случае, либо еще один объект типа QListViewItem. Если передается QListViewItem, строка создается как дочерняя по отношению к этому объекту QListViewItem. Таким образом, структура дерева формируется передачей объекта QListView для узлов верхнего уровня и затем последующих объектов типа QListViewItem для дочерних или подчиненных узлов.

Остальные параметры — данные каждого столбца, по умолчанию равные NULL, если не заданы.

Добавление дочернего узла — это просто вариант передачи в функцию указателя верхнего уровня. Если вы не добавляете последующие дочерние узлы в объект QListViewItem, нет необходимости сохранять возвращаемый указатель:

new QListViewItem(toplevel, "Left Data", "Right Data");

// Дочерний по отношению к верхнему уровню

В API QListViewItem можно найти методы обхода узлов дерева на случай корректировки конкретных строк:

#include <qlistview.h>

virtual void insertItem(QListviewitem* newChild);

virtual void setText(int column, const QString& text);

virtual QString text(int column) const;

QListViewItem* firstChild() const;

QListViewItem* nextSibling() const;

QListViewItem* parent() const;

QListViewItem* itemAbove();

QListViewItem *itemBelow();

Получить первую строку в дереве можно, вызвав метод firstChild для самого объекта QListView. Затем можно многократно вызывать firstChild и nextSibling для возврата фрагментов или целого дерева.

Приведенный далее фрагмент программного кода выводит первый столбец всех узлов верхнего уровня:

QListViewItem *child = view->firstChild();

while(child) {

 cout << myChild->text(1) << "\n";

 myChild = myChild->nextSibling();

}

Все подробности, касающиеся QListView, QListViewItem и QCheckListView, см. в документации API комплекта Qt.

Выполните упражнение 17.7.

Упражнение 17.7 Виджет QListView

В этом упражнении вы соберете все вместе и напишете короткий пример использования виджета QListView.

Давайте для краткости пропустим заголовочный файл и рассмотрим реализацию класса, файл ListView.cpp.

#include "Listview.moc"

ListView::ListView(QWidget *parent, const char *name) :

 QMainWindow(parent, name) {

 listview = new QListView(this, "listview1");

 listview->addColumn("Artist");

 listview->addColumn("Title");

 listview->addColumn("Catalogue");

 listview->setRootIsDecorated(TRUE);

 QListViewItem* toplevel = new QListViewItem(listview, "Avril Lavigne",

  "Let Go", "AVCD01");

 new QListViewItem(toplevel, "Complicated");

 new QListViewItem(toplevel, "Sk8er Boi");

 setCentralWidget(listview);

}

int main(int argc, char **argv) {

 QApplication app(argc, argv);

 ListView *window = new ListView();

 app.setMainWidget(window);

 window->show();

 return app.exec();

}

Как это работает

Виджет QListView кажется сложным, потому что он действует и как список элементов, и как дерево элементов. В вашем программном коде необходимо создать экземпляры QListViewItem для каждого элемента, включаемого вами в список. У каждого экземпляра типа QListViewItem есть родитель. Эти элементы с самим виджетом в качестве родителя отображаются как элементы верхнего уровня. Элементы с другим элементом типа QListViewItem в качестве родителя выводятся на экран как дочерние элементы. В этом примере показаны экземпляры QListViewItem со всего одним уровнем глубины, но можно создавать и деревья элементов с гораздо большей глубиной.

После компиляции и выполнения примера ListView вы увидите виджет QListView в действии, как показано на рис. 17.7.

Обратите внимание на то, как дочерние строки почтительно отступают от своих "родителей". Знаки "плюс" и "минус", указывающие на наличие скрытых или сворачивающихся строк, не представлены по умолчанию; в этом примере они задаются с помощью setRootIsDecorated.

Рис. 17.7

Диалоговые окна

До сих пор вы создавали подклассы QMainWindow для построения своих интерфейсов. Объекты QMainWindow предназначены для создания главного окна в вашем приложении, но для кратковременных диалоговых окон следует рассмотреть виджет QDialog.

Диалоговые окна хороши для ввода пользователем определенной информации, предназначенной для конкретной задачи, или передачи пользователю коротких сообщений, таких как предупреждение или сообщение об ошибке. Для таких задач лучше применять подкласс QDialog, поскольку вы получаете удобные методы формирования диалогового окна и специализированные сигналы и слоты для обработки ответов пользователя.

Наряду с обычными модальными и немодальными (или безмодальными на языке Qt) диалоговыми окнами комплект Qt также предлагает полумодальное диалоговое окно. В следующем перечне приведены отличия модальных и немодальных диалоговых окон, в него также включены полумодальные окна.

□ Модальное диалоговое окно блокирует ввод во все другие окна, чтобы заставить пользователя дать ответ в диалоговом окне. Модальные диалоговые окна полезны для захвата немедленного ответа пользователя и отображения важных сообщений об ошибках.

□ Немодальное диалоговое окно — неблокирующее окно, которое действует обычно наряду с другими окнами приложения. Немодальные диалоговые окна удобны для окон поиска или ввода, в которых вы сможете, например, копировать и вставлять значения в главное окно и из него.

□ Полумодальное диалоговое окно — это модальное окно, не имеющее своего цикла событий. Это позволяет возвращать управление приложению, но сохранять блокировку ввода для других окон. Полумодальные окна бывают полезны в редких случаях, когда у вас есть индикатор выполнения процесса важной, требующей значительного времени операции, и вы хотите дать пользователю возможность отменить ее при необходимости. Поскольку у такого окна нет собственного цикла событий, для его обновления вы должны периодически вызывать метод QApplication::processEvents.

QDialog

QDialog — базовый класс диалоговых окон в Qt, предоставляющий методы exec и show для обработки модальных и немодальных диалоговых окон, у него есть встроенный класс QLayout, который можно использовать, и несколько сигналов и слотов, полезных для формирования откликов на нажатие кнопки.

Обычно вы будете создавать для своих диалоговых окон класс-потомок QDialog и вставлять в него виджеты для создания интерфейса диалогового окна:

#include <qdialog.h>

MyDialog::MyDialog(QWidget *parent, const char *name) : QDialog(parent, name) {

 QHBoxLayout *hbox = new QHBoxLayout(this);

 hbox->addWidget(new Qlabel("Enter your name"));

 hbox->addWidget(new QLineEdit());

 hbox->addWidget(ok_pushbutton);

 hbox->addWidget(cancel_pushbutton);

 connect(ok_pushbutton, SIGNAL(clicked()), this, SLOT(accept()));

 connect(cancel_pushbutton, SIGNAL(clicked()), this, SLOT(reject()));

}

В отличие от виджета типа QMainWindow вы можете задать объект MyDialog как родительский для своего объекта QLayout без создания пустого QWidget в качестве родительского.

Примечание

Имейте в виду, что в этом примере пропущен программный код для создания виджетов ok_pushbutton и cancel_pushbutton.

У объекта QDialog есть два слота — accept и reject, которые применяются для обозначения результата, полученного в диалоговом окне. Этот результат возвращается методом exec. Как правило, вы будете связывать кнопки OK и Cancel со слотами, как в MyDialog.

Модальные диалоговые окна

Для применения диалогового окна как модального вы вызываете метод exec, который открывает диалоговое окно и возвращает QDialog::Accepted или QDialog::Rejected в зависимости от того, какой слот был активизирован:

MyDialog* dialog = new MyDialog(this, "mydialog");

if (dialog->exec() == QDialog::Accepted) {

 // Пользователь щелкнул мышью кнопку OK

 doSomething();

} else {

 // Пользователь щелкнул мышью кнопку Cancel или

 // диалоговое окно уничтожено

 doSomethingElse();

}

delete dialog;

Когда метод exec возвращает управление приложению, диалоговое окно автоматически скрывается, но вы все равно удаляете объект из памяти.

Учтите, что когда вызывается exec, вся обработка прекращается, поэтому, если в вашем приложении есть важный с точки зрения затраченного времени программный код, больше подойдут немодальное или полумодальное диалоговые окна.

Немодальные диалоговые окна

Немодальные диалоговые окна слегка отличаются от обычных основных окон прежде всего тем, что располагаются поверх своего родительского окна, совместно используют их элемент на панели задач и автоматически скрываются, когда вызван слот accept или reject.

Для отображения немодального диалогового окна вызывайте метод show, как вы сделали бы для окна QMainWindow:

MyDialog *dialog = new MyDialog(this, "mydialog");

dialog->show();

Функция show выводит диалоговое окно на экран и немедленно возвращается в приложение для продолжения цикла выполнения. Для обработки нажатой кнопки вы должны написать слоты и установить с ними связь:

MyDialog::MyDialog(QWidget *parent, const char *name) :

 QDialog(parent, name) {

 ...

 connect(ok_pushbutton, SIGNAL(clicked()), this, SLOT(OkClicked()));

 connect(cancel_pushbutton, SIGNAL(clicked()), this, SLOT(CancelClicked()));

}

MyDialog::OkClicked() {

 // Выполните что-либо

}

MyDialog::CancelClicked() {

 // Выполните что-либо другое

}

Как и в случае модального окна, диалоговое окно автоматически скрывается при нажатии кнопки.

Полумодальное диалоговое окно

Для создания полумодального диалогового окна вы должны задать флаг модального режима в конструкторе QDialog и применить метод show:

QDialog(QWidget *parent=0, const char *name=0, bool modal=FALSE, WFlags f=0);

Вы не задаете в модальном диалоговом окне флаг модального режима равным TRUE, потому что вызов exec заставляет диалоговое окно перейти в модальный режим независимо от значения этого флага.

Конструктор вашего диалогового окна будет выглядеть примерно следующим образом:

MySMDialog::MySMDialog(QWidget *parent, const char *name):

 QDialog(parent, name, TRUE) {

 ...

}

После того как вы определили ваше диалоговое окно, вызовите функцию show как обычно и затем продолжите свою обработку, периодически вызывая QApplication::processEvents для обновления вашего диалогового окна:

MySMDialog *dialog = MySMDialog(this, "semimodal");

dialog->show();

while (processing) {

 doSomeProcessing();

 app->processEvents();

 if (dialog->wasCancelled()) break;

}

Перед продолжением выполнения проверьте, не уничтожено ли диалоговое окно. Имейте в виду, что функция wasCancelled не является частью класса QDialog — вы должны написать ее самостоятельно.

Комплект Qt предоставляет готовые подклассы класса QDialog, предназначенные для конкретных задач, таких как выбор файлов, ввод текста, индикация процесса выполнения и вывод окна сообщения. Применение этих виджетов в любых приложениях убережет вас от множества неприятностей и проблем.

QMessageBox

QMessageBox — модальное диалоговое окно, отображающее простое сообщение с пиктограммой и кнопками. Пиктограмма зависит от серьезности сообщения, которое может содержать обычные сведения или предупреждения и другую важную информацию.

У класса QMessageBox есть статические методы создания и отображения окон всех трех перечисленных типов:

#include <qmessagebox.h>

int information(QWidget *parent, const QString& caption,

 const QString&text, int button0, int button1=0, int button2=0);

int warning(QWidget *parent, const QString& caption,

 const QString& text, int button0, int button1, int button2=0);

int critical(QWidget *parent, const QString& caption,

 const QString& text, int button0, int button1, int button2=0);

Можно выбрать кнопки из списка готовых кнопок QMessageBox, соответствующих значениям, возвращаемым статическими методами:

QMessageBox::Ok;

QMessageBox::Cancel;

QMessageBox::Yes;

QMessageBox::No;

QMessageBox::Abort;

QMessageBox::Retry;

QMessageBox::Ignore.

Типичный пример использования окна QMessageBox будет похож на приведенный далее фрагмент программного кода:

int result = QMessageBox::information(this,

 "Engine Room Query",

 "Do you wish to engage the HyperDrive?",

 QMessageBox::Yes | QMessageBox::Default,

 QMessageBox::No | QMessageBox::Escape);

switch (result) {

case QMessageBox::Yes:

 hyperdrive->engage();

 break;

case QMessageBox::No:

 // сделайте что-нибудь еще

 break;

}

Вы соединили операцией OR (|) коды кнопок с вариантами Default и Escape, чтобы задать стандартные действия, при нажатии клавиш <Enter> (или <Return>) и <Esc>. Результирующее диалоговое окно показано на рис. 17.8.

Рис. 17.8 

QInputDialog

Окно QInputDialog полезно для ввода пользователем отдельных значений, будь то текст, вариант раскрывающегося списка, целочисленное или действительное значение. У класса QInputDialog есть статические методы, например QMessageBox, создающие некоторые проблемы, поскольку у них слишком много параметров, к счастью, у большинства из них есть значения по умолчанию:

#include <qinputdialog.h>

QString getText(const QString& caption, const QString& label,

 QLineEdit::EchoMode mode=QLineEdit::Normal,

 const QString& text=QString::null,

 bool* ok = 0, QWidget* parent = 0, const char * name = 0);

QString getItem(const QString& caption, const QString& label,

 const QStringList& list, int current=0, bool editable=TRUE,

 bool* ok=0, QWidget* parent = 0, const char* name=0)

int getInteger(const QString& caption, const QString& label,

 int num=0, int from = -2147483647, int to = 2147483647,

 int step = 1, bool* ok = 0, QWidget* parent = 0, const char* name = 0);

double getDouble(const QString& caption, const QString& label,

 double num = 0, double from = -2147483647, double to = 2147483647,

 int decimals = 1, bool* ok = 0, QWidget* parent = 0, const char* name = 0);

Для ввода строки текста напишите следующий фрагмент кода:

bool result;

QString text = QInputDialog::getText("Question", "What is your Quest?:",

 QLineEdit::Normal, QString::null, &result, this, "input");

if (result) {

 doSomething(text);

} else {

 // Пользователь нажал Cancel

}

Как видно из рис. 17.9, окно QInputDialog создано с помощью виджета QLineEdit и кнопок OK и Cancel.

Рис. 17.9 

Диалоговое окно, созданное методом QInputDialog::getText, применяет виджет QLineEdit. Параметр режима редактирования, передаваемый в функцию getText, управляет способом отображения набираемого текста точно так же, как аналогичный параметр режима виджета QLineEdit. Вы можете также задать текст, выводимый по умолчанию, или оставить поле пустым, как показано на рис. 17.9. У всех окон QInputDialog есть кнопки OK и Cancel, и в метод передается указатель типа bool для обозначения нажатой кнопки — результат равен TRUE, если пользователь щелкает мышью кнопку OK.

Метод getItem с помощью раскрывающегося списка QComboBox предлагает пользователю список вариантов:

bool result;

QStringList options;

options << "London" << "New York" << "Paris";

QString city = QInputDialog::getItem("Holiday", "Please select a

 destination:", options, 1, TRUE, &result, this, "combo");

if (result) selectDestination(city);

Созданное диалоговое окно показано на рис. 17.10.

Рис. 17.10 

Функции getInteger и getDouble действуют во многом аналогично, поэтому мы не будем на них останавливаться.

Применение qmake для упрощения написания make-файлов

Компиляция приложения с библиотеками KDE и Qt становится утомительным занятием, поскольку ваш make-файл получается очень сложным из-за необходимости использовать moc и иметь библиотеки здесь, там и везде. К счастью, Qt поставляется с утилитой qmake для создания ваших make-файлов.

Примечание

Если вы уже пользовались комплектом Qt, вам, возможно, знакома утилита tmake — более раннее (и теперь устаревшее) воплощение qmake, поставлявшееся с предыдущими версиями Qt.

Утилита qmake принимает в качестве входного файл .pro. Этот файл содержит самые существенные сведения, необходимые для компиляции, такие как исходные тексты, заголовочные файлы, результирующий двоичный файл и местонахождения библиотек KDE/Qt.

Типичный pro-файл среды KDE выглядит следующим образом:

TARGET = app

MOC_DIR = moc

OBJECTS_DIR = obj

INCLUDEPATH = /usr/include/kde

QMAKE_LIBDIR_X11 += /usr/lib

QMAKE_LIBS_X11 += -lkdeui -lkdecore

SOURCES = main.cpp window.cpp

HEADERS = window.h

Вы задаете результирующий двоичный файл, временные каталоги moc и объектных файлов, путь к библиотеке KDE и исходные тексты, и заголовочные файлы, из которых формируется приложение. Учтите, что местонахождение файлов библиотек и заголовочных файлов KDE зависит от вашего дистрибутива. Пользователи SUSE должны приравнять INCLUDEPATH путь /opt/kde3/include и QMAKE_LIBS_X11 путь /opt/kde3/lib.

$ qmake file.pro -о Makefile

Затем вы можете выполнить команду make как обычно, что не вызовет затруднений. Для упрощения процедуры построения приложения следует использовать qmake с программами любой сложности, применяющими KDE/Qt.

Создание меню и панелей инструментов с помощью KDE

Для того чтобы продемонстрировать мощь виджетов KDE, мы оставили меню и панели инструментов напоследок, поскольку они — уж очень наглядные примеры того, как библиотеки KDE экономят время и усилия по сравнению с применением только Qt или любых других комплектов с элементами графического пользовательского интерфейса.

Обычно в библиотеках GUI элементы меню и панелей инструментов — отличающиеся элементы, каждый со своим собственным виджетом. Вы должны создавать отдельные объекты для каждого элемента и отслеживать изменения, например, делая недоступными определенные варианты, каждый отдельно.

У программистов KDE появилось лучшее решение. Вместо такого обособленного подхода в KDE определен виджет KAction для представления действия, которое может выполнить приложение. Это действие может открыть новый документ, сохранить файл или вывести окно справки.

KAction присваивается текст, клавиатурный акселератор, пиктограмма и слот, который вызывается при активизации действия:

KAction *new_file = new KAction("New", "filenew",

 KstdAccel::shortcut(KstdAccel::New), this,

 SLOT(newFile()), this, "newaction");

Затем KAction может быть вставлено в меню и панель инструментов без дополнительного описания:

new_file->plug(a_menu);

new_file->plug(a_toolbar);

Таким образом, вы создали пункт меню New и кнопку панели инструментов, которые вызывают newFile при щелчке кнопкой мыши.

Теперь если вам нужно отменить KAction — скажем, вы не хотите, чтобы пользователь мог создать новый файл, — вызов централизован:

new_file->setEnabled(FALSE);

Это все, что касается меню и панелей инструментов в среде KDE — на самом деле очень легко и просто. Далее приведен конструктор виджета KAction:

#include <kde/kaction.h>

KAction(const QString& text, const KShortcut& cut,

 const QObject* receiver, const char* slot,

 QObject *parent, const char* name = 0);

KDE предоставляет стандартные объекты KAction для унификации текста, клавиатурных акселераторов и пиктограмм в разных приложениях KDE:

#include <kde/kaction.h>

KAction* openNew(const QObject* recvr, const char *slot,

 KActionCollection* parent, const char* name = 0)ж

KAction* save ...

KAction* saveAs ...

KAction* revert ...

KAction* close ...

KAction* print ...

И т.д.

Любое стандартное действие принимает одни и те же параметры; слот-приемник и функцию, KActionCollection и имя KAction. Объект KActionCollection управляет в окне объектами KAction, и вы можете получить текущий объект с помощью метода actionCollection окна KMainWindow:

KAction *saveas = KStdAction::saveAs(this, SLOT(saveAs()) ,

 actionCollection(), "saveas");

Выполните упражнение 17.8.

Упражнение 17.8. Приложение в KDE с меню и панелями инструментов

В приведенном далее примере вы опробуете объекты KAction в приложении среды KDE.

1. Начните с заголовочного файла KDEMenu.h. KDEMenu — это подкласс KMainWindow, являющегося подклассом класса QMainWindow. KMainWindow управляет сеансом в среде KDE и обладает встроенными панелью инструментов и строкой состояния.

#include <kde/kmainwindow.h>

class KDEMenu : public KMainWindow {

 Q_OBJECT

public:

 KDEMenu(const char * name = 0);

private slots:

 void newFile();

 void aboutApp();

};

2. Файл KDEMenu.cpp начните с директив #include для виджетов, которые будете применять:

#include "KDEMenu.h"

#include <kde/kapp.h>

#include <kde/kaction.h>

#include <kde/kstdaccel.h>

#include <kde/kmenubar.h>

#include <kde/kaboutdialog.h>

3. В конструкторах, создающих три виджета KAction, new_file определяется вручную, a quit_action и help_action используют стандартные определения KAction:

KDEMenu::KDEMenu(const char *name = 0) : KMainWindow (0L, name) {

 KAction *new_file = new KAction("New", "filenew",

  KstdAccel::shortcut(KstdAccel::New), this, SLOT(newFile()),

  this, "newaction");

 KAction *quit_action = KStdAction::quit(KApplication::kApplication(),

  SLOT(quit()), actionCollection());

 KAction *help_action = KStdAction::aboutApp(this, SLOT(aboutApp()),

  actionCollection());

4. Создайте два меню верхнего уровня и включите их в строку меню KApplication:

QPopupMenu *file_menu = new QPopupMenu;

QPopupMenu *help_menu = new QPopupMenu;

menuBar()->insertItem("&File", file_menu);

menuBar()->insertItem("&Help", help_menu);

5. Теперь вставьте действия в меню и панель инструментов, добавив разделительную линию между new_file и quit_action:

 new_file->plug(file_menu);

 file_menu->insertSeparator();

 quit_action->plug(file_menu);

 help_action->plug(help_menu);

 new_file->plug(toolBar());

 quit_action->plug(toolBar());

}

6. В заключение несколько определений слотов: aboutApp создает диалоговое окно KAbout для отображения сведений о программе. Учтите, что слот quit определен как часть KApplication:

void KDEMenu::newFile() {

 // Создание нового файла

}

void KDEMenu::aboutApp() {

 KAboutDialog *about = new KAboutDialog(this, "dialog");

  about->setAuthor(QString("A. N. Author"),

  QString("an@email.net"), QString("http://url.com"),

  QString("work"));

 about->setVersion("1.0");

 about->show();

}

int main(int argc, char **argv) {

 KApplication app(argc, argv, "cdapp");

 KDEMenu* window = new KDEMenu("kdemenu");

 app.setMainWidget(window);

 window->show();

 return app.exec();

} 

7. Далее вам нужен файл menu.pro для утилиты qmake:

TARGET = kdemenu

MOC_DIR = moc

OBJECTS_DIR = obj

INCLUDEPATH = /usr/include/kde

QMAKE_LIBDIR_X11 += -L$KDEDIR/lib

QMAKE_LIBS_X11 += -lkdeui -lkdecore

SOURCES = KDEMenu.cpp

HEADERS = KDEMenu.h

8. Теперь запустите qmake для создания make-файла, откомпилируйте и выполните программу:

$ qmake menu.pro -о Makefile

$ make

$ ./kdemenu

Как это работает

Несмотря на то, что этот пример получился чуть длиннее других, программный код довольно краток, если учесть всю выполняемую им работу по созданию строки меню и самих меню. Лучшее качество виджетов KAction — возможность использования каждого из них в разных частях программы, таких как панель инструментов и меню в строке меню, все упомянутые возможности показаны в данном примере.

Построение приложений KDE требует больше работы, чем создание большинства программ, по крайней мере, на первый взгляд. В действительности файл menu.pro и команда qmake скрывают большой набор параметров, которые в противном случае вам пришлось бы вставлять вручную в ваш make-файл.

На рис. 17.11 и 17.12 показано, как появляются в окне меню и кнопки панели инструментов.

Рис. 17.11 

Рис. 17.12 

И вот оно! Мы закончили наш тур по Qt и KDE, рассмотрев базовые элементы, всех приложений GUI, окна, схемы размещения, кнопки, диалоговые окна и меню. Существует бесчисленное множество виджетов Qt и KDE, о которых мы не упоминали, начиная с QColorDialog — диалогового окна для выбора цвета — и заканчивая KHTML — виджетом Web-обозревателя — все они подробно описаны на Web-сайтах компании Trolltech и графической среды KDE. 

Приложение для работы с базой данных компакт-дисков с использованием KDE/Qt

Теперь, когда вы можете использовать силу и мощь KDE/Qt, пришло время снова обратить внимание на приложение для работы с компакт-дисками, чтобы привести его в чувство.

Напоминаем, чего вы хотите добиться от вашего приложения для работы с базой данных компакт-дисков:

□ регистрация в базе данных из графического пользовательского интерфейса;

□ поиск компакт-диска в базе данных;

□ вывод информации о компакт-диске и его дорожках;

□ добавление компакт-диска в базу данных;

□ отображение окна About (О программе).

MainWindow

Начнем обсуждение с программного кода для главного окна приложения, которое содержит виджет поля поиска и список для отображения результатов поиска.

1. Начните с ввода программного кода в файл MainWindow.h (или загрузите его с Web-сайта книги). Поскольку окно содержит виджет QLineEdit для поиска компакт-дисков и виджет QListView для вывода результатов поиска, вы должны вставить в программный код заголовочные файлы qlistview.h и qlineedit.h:

#include <kde/kmainwindow.h>

#include <qlistview.h>

#include <qlineedit.h>

class MainWindow : public KMainWindow {

 Q_OBJECT

public:

 MainWindow(const char *name);

public slots:

 void doSearch();

 void Added();

private:

 QListView *list;

 QLineEdit *search_entry;

};

2. MainWindow.срр — самая сложная часть программы. В конструкторе вы создаете интерфейс главного окна и связываете необходимые сигналы с вашими слотами. Как обычно, начните программу с файлов в директивах #include:

#include "MainWindow.h"

#include "AddCdDialog.h"

#include "app_mysql.h"

#include <qvbox.h>

#include <qlineedit.h>

#include <qpushbutton.h>

#include <qlabel.h>

#include <qlistview.h>

#include <kde/kapp.h>

#include <kde/kmenubar.h>

#include <kde/klocale.h>

#include <kde/kpopupmenu.h>

#include <kde/kstatusbar.h>

#include <kde/kaction.h>

#include <kde/kstdaccel.h>

#include <string.h>

MainWindow::MainWindow(const char * name) : KMainWindow(0L, name) {

 setCaption("CD Database");

3. Теперь создайте элементы меню и панели инструментов с помощью виджета KAction.

KAction *addcd_action = new KAction("&Add CD", "filenew",

 KStdAccel::shortcut(KStdAccel::New), this, SLOT(AddCd()), this);

 KAction *quit_action = KStdAction::quit(KApplication::kApplication(),

  SLOT(quit()), actionCollection());

 QPopupMenu* filemenu = new QPopupMenu;

 QString about = ("CD App\n\n"

  "(C) 2007 Wrox Press\n" "email@email.com\n");

 QPopupMenu* helpmenu = helpMenu(about);

 menuBar()->insertItem("&File", filemenu);

 menuBar()->insertltem(i18n("&Help"), helpmenu);

 addcd_action->plug(filemenu);

 filemenu->insertSeparator();

 quit_action->plug(filemenu);

 addcd_action->plug(toolBar());

 quit_action->plug(toolBar());

4. Для разнообразия примените виджеты QBoxLayout вместо обычных классов QLayout:

 QVBox *vbox = new QVBox(this);

 QHBox *hbox = new QHBox(vbox);

 QLabel* label = new QLabel(hbox);

 label->setText("Search Text: ");

 search_entry = new QLineEdit(hbox);

 QPushButton *button = new QPushButton("Search", hbox);

5. Далее следует виджет QListView, занимающий основную часть рабочей области окна. После этого для поиска компакт-диска в базе данных вы связываете необходимые сигналы с вашим слотом doSearch. Строка состояния KMainWindow становится видимой за счет вставки пустого сообщения:

 list = new QListView(vbox, "name", 0L);

 list->setRootIsDecorated(TRUE);

 list->addColumn("Title");

 list->addColumn("Artist");

 list->addColumn("Catalogue");

 connect(button, SIGNAL(clicked()), this, SLOT(doSearch()));

 connect(search_entry, SIGNAL(returnPressed()), this, SLOT(doSearch()));

 statusBar()->message("");

 setCentralWidget(vbox);

 resize(300, 400);

}

6. Слот doSearch — рабочее завершение приложения. В нем считывается строка поиска и выбираются все соответствующие ей компакт-диски и их дорожки. Логика слота такая же, как в функции doSearch GNOME/GTK+ в главе 16.

void MainWindow::doSearch() {

 cd_search_st *cd_res = new cd_search_st;

 current_cd_st *cd = new current_cd_st;

 struct current_tracks_st ct;

 int res1, i, j, res2, res3;

 char track_title[110];

 char search_text[100];

 char statusBar_text[200];

 QListViewItem *cd_item;

 strcpy(search_text, search_entry->text());

7. Извлеките id соответствующих компакт-дисков и обновите строку состояния, чтобы отобразить результаты поиска:

 res1 = find_cds(search_text, cd_res);

 sprintf(statusBar_text,

  " Displaying %d result(s) for search string ' %s'",

  res1, search_text);

 statusBar()->message(statusBar_text);

 i = 0;

 list->clear();

8. Для каждого id извлеките сведения о компакт-диске в виджет QListView и информацию обо всех дорожках данного CD:

 while (i < res1) {

  res2 = get_cd(cd_res->cd_id[i], cd);

  cd_item = new QListViewItem(list, cd->title, cd->artist_name,

   cd->catalogue);

  res3 = get_cd_tracks(cd_res->cd_id[i++], &ct);

  j = 0;

  /* Заполните дерево дорожками текущего компакт-диска */

  while (j < res3) {

   sprintf(track_title, " Track %d. ", j+1);

   strcat(track_title, ct.track[j++]);

   new QListViewItem(cd_item, track_title);

  }

 }

}

Рис. 17.13

9. Слот AddCd вызывается, когда активизирован пункт меню или кнопка панели инструментов addcd_action:

void MainWindow::AddCd()

 AddCdDialog* dialog = new AddCdDialog(this);

 dialog->show();

}

Результат показан на рис. 17.13.

AddCdDialog

Для вставки сведений о компакт-диске в базу данных вам нужно включить в программу диалоговое окно с полями, необходимыми для ввода информации.

1. Введите следующий программный код в файл AddCdDialog.h. Имейте в виду, что класс AddCdDialog — потомок KDialogBase, виджета диалогового окна в среде KDE.

#include <kde/kdialogbase.h>

#include <qlineedit.h>

class AddCdDialog : public KDialogBase {

 Q_OBJECT

public:

 AddCdDialog(QWidget* parent);

private:

 QLineEdit* artist_entry, *title_entry, *catalogue_entry;

public slots:

 void okClicked();

};

2. Далее следует файл AddCdDialog.cpp, в котором в слоте okClicked вызывается функция add_cd из интерфейса MySQL:

#include "AddCdDialog.h"

#include "app_mysql.h"

#include <qlayout.h>

#include <qlabel.h>

AddCdDialog::AddCdDialog(QWidget* parent)

 : KDialogBase(parent, "AddCD", false, "Add CD",

 KDialogBase::Ok | KDialogBase::Cancel, KDialogBase::Ok, true) {

 QWidget *widget = new QWidget(this);

 setMainWidget(widget);

 QGridLayout *grid = new QGridLayout(widget, 3, 2, 10, 5, "grid");

 grid->addWidget(new QLabel("Artist", widget, "artistlabel"), 0, 0, 0);

 grid->addWidget(new QLabel("Title", widget, "titlelabel"), 1, 0, 0);

 grid->addwidget(new QLabel("Catalogue", widget, "cataloguelabel"), 2, 0, 0);

 artist_entry = new QLineEdit(widget, "artist_entry");

 title_entry = new QLineEdit(widget, "titleentry");

 catalogue_entry = new QLineEdit(widget, "catalogue_entry");

 grid->addWidget(artist_entry, 0, 1, 0);

 grid->addWidget(title_entry, 1, 1, 0);

 grid->addWidget(catalogue_entry, 2, 1, 0);

 connect(this, SIGNAL(okClicked()), this, SLOT(okClicked()));

}

void AddCdDialog::okClicked() {

 char artist[200];

 char title[200];

 char catalogue[200];

 int cd_id = 0;

 strcpy(artist, artist_entry->text());

 strcpy(title, title_entry->text());

 strcpy(catalogue, catalogue_entry->text());

 add_cd(artist, title, catalogue, &cd_id);

}

На рис. 17.14 показано работающее диалоговое окно AddCdDialog.

Рис. 17.14 

LogonDialog

Вы, конечно же, не сможете запрашивать базу данных без предварительной регистрации, поэтому вам необходимо простое диалоговое окно для ввода имени пользователя и пароля. Назовите этот класс LogonDialog. (Да, еще один пример остроумного и образного имени!)

1. Начнем с заголовочного файла. Введите приведенный далее программный код и назовите файл LogonDialog.h. Обратите внимание на то, что для разнообразия этот класс описан в данном случае как потомок класса QDialog, а не KDialogBase.

#include <qdialog.h>

#include <qlineedit.h>

class LogonDialog : public QDialog {

 Q_OBJECT

public:

 LogonDialog(QWidget* parent = 0, const char *name = 0);

 QString getUsername();

QString getPassword();

private:

 QLineEdit *username_entry, *password_entry;

};

2. У вас есть более удобные методы для имени пользователя и пароля, чем инкапсуляция в файле LogonDialog.cpp вызова database_start. Далее приведен файл LogonDialog.cpp:

#include "LogonDialog.h"

#include "appmysql.h"

#include <qpushbutton.h>

#include <qlayout.h>

#include <qlabel.h>

LogonDialog::LogonDialog(QWidget *parent, const char *name):

 QDialog(parent, name) {

 QGridLayout *grid = new QGridLayout(this, 3, 2, 10, 5, "grid");

 grid->addWidget(new QLabel("Username", this, "usernamelabel"), 0, 0, 0);

 grid->addWidget(new QLabel("Password", this, "passwordlabel"), 1, 0, 0);

 username_entry = new QLineEdit(this, "username entry");

 password_entry = new QLineEdit(this, "password_entry");

 password_entry->setEchoMode(QLineEdit::Password);

 grid->addWidget(username_entry, 0, 1, 0);

 grid->addWidget(passwordentry, 1, 1, 0);

 QPushButton* button = new QPushButton("Ok", this, "button");

 grid->addWidget(button, 2, 1, Qt::AlignRight);

 connect (button, SIGNAL(clicked()), this, SLOT(accept()));

}

QString LogonDialog::getUsername() {

 if (username_entry == NULL) return NULL;

 return username_entry->text();

}

QString LogonDialog::getPassword() {

 if (password_entry == NULL) return NULL;

 return password_entry->text();

}

На рис. 17.15 показано, как будет выглядеть диалоговое окно.

Рис. 17.15 

main.cpp

Единственный оставшийся программный код — функция main, которую вы помещаете в отдельный файл main.cpp.

1. В файле main.cpp вы открываете окно LogonDialog и получаете успешную регистрацию из функции database_start. Если регистрация оказалась неудачной, вы выводите окно QMessageBox или при попытке закрыть LogonDialog просите у пользователя подтверждения его выхода.

#include "MainWindow.h"

#include "app_mysql.h"

#include "LogonDialog.h"

#include <kde/kapp.h>

#include <qmessagebox.h>

int main(int argc, char **argv) {

 char username[100];

 char password[100];

 KApplication a(argc, argv, "cdapp");

 LogonDialog *dialog = new LogonDialog();

 while (1) {

  if (dialog->exec() == QDialog::Accepted) {

   strcpy(username, dialog->getUsername());

   strcpy(password, dialog->getPassword());

   if (database_start(username, password)) break;

   QMessageBox::information(0, "Title",

    "Could not Logon: Check username and/or password",

    QMessageBox::Ok);

   continue;

  } else {

   if (QMessageBox:information(0, "Title",

    "Are you sure you want to quit?", QMessageBox::Yes,

    QMessageBox::No) == QMessageBox::Yes) {

    return 0;

   }

  }

 }

 delete dialog;

 MainWindow *window = new MainWindow("Cd App");

 window->resize(600, 400);

 a.setMainWidget(window);

 window->show();

 return a.exec();

}

2. Осталось только написать pro-файл для утилиты qmake. Назовите его cdapp.pro:

TARGET = app

MOC_DIR = moc

OBJECTS_DIR = obj

INCLUDEPATH = /usr/include/kde /usr/include/mysql

QMAKE_LIBDIR_X11 += -/usr/lib

QMAKE_LIBDIR_X11 += /usr/lib/mysql

QMAKE_LIBS_X11 += -lkdeui -lkdecore -lmysqlclient

SOURCES = MainWindow.cpp main.cpp app_mysql.cpp AddCdDialog.cpp LogonDialog.cpp

HEADERS = MainWindow.h app_mysql.h AddCdDialog.h LogonDialog.h

Примечание

Обратите внимание на то, что приведенный программный код позволяет вам немного схитрить, просто переименовав файл app_mysql.c в файл app_mysql.cpp; таким образом, вы сможете использовать его как обычный исходный файл на языке С++. Это устраняет небольшое усложнение, необходимость редактирования связей или компоновки объектного файла на языке С и объектного файла на языке С++,

$ qmake cdapp.pro -о Makefile

$ make

$ ./арр

Если все нормально, вы должны получить работающую базу данных компакт-дисков!

Для того чтобы глубже понять KDE/Qt, можно попробовать реализовать другие функции в интерфейсе MySQL, такие как добавление дорожек в компакт-диски или удаление компакт-дисков. Вам понадобятся диалоговые окна, новые элементы меню и панели инструментов, придется также запрограммировать внутреннюю логику. Сделайте такую попытку!

Резюме 

В этой главе вы научились применять библиотеку графического пользовательского интерфейса Qt и рассмотрели виджеты графической среды KDE в действии. Вы узнали, что Qt — это библиотека на языке С++, применяющая механизм "сигнал/слот" для реализации программирования, управляемого событиями. Вы познакомились с основными виджетами Qt и написали несколько программ-примеров для демонстрации их практического применения. В заключение средствами KDE/Qt реализован графический пользовательский интерфейс вашего приложения для работы с компакт-дисками. 

Глава 18

Стандарты Linux

ОС Linux появилась сначала только как ядро системы. К сожалению, ядро само по себе не очень полезно; программам нужна регистрация, управление файлами, компиляция новых программ и т.д. Для того чтобы сделать систему полезной, в рамках проекта GNU были добавлены разные средства. Они представляли собой клоны похожих программ, имевшихся в UNIX и UNIX-подобных системах того времени. Превращение системы Linux в подобие UNIX-системы установило первые стандарты для Linux, предоставляя программистам на языке С знакомую рабочую среду.

Разные разработчики ОС UNIX (а позднее Linux) вставляли собственные расширения в команды и утилиты, которые включали в состав системы, и структура используемых ими файловых систем тоже слегка отличалась. Все это затрудняло создание приложений, способных выполняться в разных системах. Более того, программист не мог даже полагаться на то, что функциональные возможности системы были реализованы одинаково, или файлы конфигурации хранились в одном и том же месте.

Стало ясно, что для сохранения подобия UNIX-систем нужна стандартизация, и такая работа сейчас ведется.

Со временем не только стандарты двигались вперед, но и ОС Linux с впечатляющей скоростью совершенствовалась сообществом, поддержанным коммерческими организациями, такими как Red Hat и Canonical, и даже разработчиками не-Linux, например, корпорацией IBM. По мере развития Linux наряду с разработкой коллекции компиляторов gcc не только следила за соответствующими стандартами, но и определяла новые стандарты, если существующие оказывались неэффективными. В действительности по мере того, как ОС Linux и связанные с нею программные средства и утилиты становились все более популярными, разработчики UNIX-систем начали вносить изменения в свои продукты, чтобы сделать их более совместимыми с ОС Linux.

В этой заключительной главе мы собираемся рассмотреть стандарты Linux, обращая внимание нате области, о которых вы должны знать для того, чтобы не только писать приложения, работающие в ваших системах Linux после их обновления, но и создавать программный код, который можно будет переносить в другие дистрибутивы Linux, а может быть и в UNIX-подобные системы, обеспечивая, таким образом, совместное использование ваших программ.

В особенности мы коснемся следующих тем:

□ стандарт языка программирования С;

□ стандарты UNIX, в особенности POSIX, разрабатываемые IEEE, и стандарт Single UNIX Specification, разработанный Open Group;

□ разработка Free Standards Group, в особенности Linux Standard Base, в которой определен макет стандартной файловой системы Linux.

Хорошей отправной точкой для знакомства со стандартами, относящимися к ОС Linux, служит стандарт Linux Standard Base (LSB), который можно найти на Web- сайте Linux Foundation по адресу http://www.linux-foundation.org/.

Мы не собираемся подробно рассматривать содержание стандартов, многие из которых по объему сравнимы с данной книгой. Мы хотим обратить ваше внимание на ключевые стандарты, о которых следует знать, дать вам краткие исторические сведения о том, как развивались эти стандарты, и помочь решить, какие из них могут оказаться полезными при написании ваших собственных программ.

Язык программирования С

Язык программирования С — de facto язык программирования ОС Linux, поэтому, для того чтобы писать программы на С для Linux, необходимо немного разобраться в его истоках, узнать, как менялся язык, и, что особенно важно понять, как проверяются программы на соответствие стандартам.

Краткий урок истории

Тем, кто не слишком любит историю, не стоит беспокоиться: эта книга о программировании, а не об истории, поэтому обзор будет очень кратким.

Язык программирования С появился в начале 1970-х годов и был основан отчасти на более раннем языке программирования BCPL и расширениях для языка В. Деннис Ритчи (Dennis М. Ritchie) написал руководство пользователя для языка в 1974 г., и примерно в это же время С был использован как язык программирования для переработки ядра UNIX на компьютерах PDP-11. В 1978 г. Брайан Керниган (Brian W. Kernighan) и Ритчи написали классическое руководство по, языку "The С Programming Language" ("Язык программирования С").

Очень быстро язык приобрел большую популярность, обусловленную, несомненно, отчасти быстрым ростом популярности UNlX-систем, но также и своими возможностями и понятным синтаксисом. Синтаксис языка С продолжал развиваться согласованно, но по мере того, как он изменялся все больше и больше по сравнению с первоначальным описанием, приведенным в книге, становилось ясно, что нужен стандарт, который соответствовал бы современному употреблению и был более строгим.

В 1983 г. ANSI (American National Standards Institute, Американский институт стандартов) основал комитет стандартов X3J11 для разработки четкого и строгого определения языка. Попутно обе организации вносили в язык незначительные изменения, в особенности придавая ему долгожданную способность объявлять типы параметров, но в основном комитет просто вносил ясность и логическое обоснование существующего определения того, что составляло общеупотребительный вариант языка. Окончательный стандарт был опубликован в 1989 г. как ANSI Standard Programming Language С, X3.159-1989 или более кратко C89, иногда именуемый C90. (Этот последний превратился в стандарт ISO/IEC 9899:1990, Programming Languages — С. Оба стандарта формально идентичны.)

Как и для большинства стандартов, публикация не закончила работу комитета, который продолжал устранять некоторые неточности, обнаруженные в спецификации, и в 1993 г. начал работу над новой версией стандарта, названного C9X. Комитет также публиковал, незначительные корректировки и обновления существующего стандарта в 1994-1996 гг.

Новая версия стандарта была сделана в 1990 гг. и официально стала стандартом С99; она была принята ISO как стандарт ISO/IEC 9899:1999. До сих пор существует работающий комитет J11, который следит за стандартизацией языка С и его библиотек, но теперь он работает под управлением группы International Committee for Information Technology Standards (Международный комитет по промышленным стандартам в сфере информационных технологий). Дополнительную информацию о работе по стандартизации С см. на Web-сайте http://j11.incits.org/.

Коллекция компиляторов GNU

После разработки редактора Emacs (да, мы любим Emacs) следующим важным достижением проекта GNU, как упоминалось в главе 1, стал полностью бесплатный компилятор С, gcc, первая официальная версия которого была выпущена в 1987 г.

Первоначально имя gcc расшифровывалось как GNU С Compiler (компилятор С проекта GNU), но, поскольку базовая рабочая среда компилятора теперь поддерживает много других языков программирования, таких как С++, Objective-C, FORTRAN, Java и Ada, а также библиотеки для этих языков, определение было заменено на более подходящее GNU Compiler Collection (коллекция компиляторов GNU).

gcc всегда был и похоже останется стандартным компилятором для Linux и С или С++, основного языка для написания программ в ОС Linux. Исходную страницу gcc можно найти по адресу http://gcc.gnu.org/.

Компилятор С GNU всегда хорошо отслеживал развитие стандарта языка С, хотя он допускает некоторые расширения языка, и, безусловно, существуют незначительные задержки, как почти у всех компиляторов, между выходом стандарта и появлением версий компиляторов, точно следующих этой спецификации. Порой происходит обратное, и gcc предвосхищает слабые изменения стандарта, что тоже может совершенно сбивать с толку. У gcc есть ряд опций командной строки и других, позволяющих задать версию стандарта языка С, которой должен соответствовать компилятор, а также ряд других опций для управления степенью придирчивости или строгости компилятора.

Опции gcc

Теперь, когда вы узнали кое-что о стандарте С, давайте рассмотрим опции, которые предлагает компилятор gcc для гарантии соответствия стандарту языка С, на котором вы пишете. Есть три способа, позволяющих убедиться в том, что ваш код на С соответствует стандартам и не содержит изъянов: опции, контролирующие версию стандарта, соответствия с которой вы намерены добиваться, определения, контролирующие заголовочные файлы, и опции предупреждений, инициирующие более строгую проверку программного кода.

У gcc есть огромный набор опций, и здесь мы рассмотрим лишь те из них, которые считаем наиболее важными. Полный перечень опций можно найти на страницах интерактивного справочного руководства gcc. Мы также кратко обсудим некоторые опции директивы #define, которые можно применять; обычно их следует задавать в вашем исходном программном коде перед любыми строками с директивой #include или определять в командной строке gcc. Вас может удивить такое обилие опций для выбора применяемого стандарта вместо простого флага, заставляющего использовать современный стандарт. Причина заключается в том, что много более старых программ полагается на исторически сложившееся поведение компилятора и потребовалась бы значительная работа по их обновлению в соответствии с последними стандартами. Редко, если вообще когда-нибудь, вам захочется обновить компилятор для того, чтобы он начал прерывать работающий программный код. По мере изменения стандартов важно иметь возможность работать вопреки определенному стандарту, даже если это и не самая свежая версия стандарта.

Даже если вы пишете маленькую программу для личного применения, когда соответствие стандартам, возможно, не так уж важно, часто имеет смысл включить дополнительные предупреждения gcc, чтобы заставить компилятор искать ошибки в вашем коде еще до выполнения программы. Это всегда эффективнее, чем выполнять по шагам код в отладчике и недоумевать по поводу возможного места возникшей проблемы. У компилятора есть много опций, которые не ограничиваются простой проверкой на соответствие стандартам, таких, как способность обнаруживать код, который удовлетворяет стандарту, но, возможно, имеет сомнительную семантику. Например, в программе может быть такой порядок выполнения, который позволяет обращаться к переменной до ее инициализации.

Если вам нужно написать программу для коллективного использования, при выбранных степени соответствия стандарту и типах предупреждений компилятора, которые вы считаете достаточными, очень важно затратить немного больше усилий и добиться компиляции вашего кода без каких-либо предупреждений вообще. Если вы допустите наличие некоторых предупреждений и привыкните их игнорировать, в один прекрасный день может появиться более серьезное предупреждение, которое вы рискуете пропустить. Если ваш программный код всегда компилируется без предупреждающих сообщений, новое предупреждение неизбежно привлечет ваше внимание. Компиляция программного кода без предупреждений — полезная привычка, которую стоит взять на вооружение.

Опции компилятора для отслеживания стандартов

Приведенные далее опции передаются gcc в командной строке; мы перечисляем здесь только самые важные из них.

□ -ansi — это самая важная опция, касающаяся стандартов и заставляющая компилятор действовать в соответствии со стандартом языка ISO C90. Она отключает некоторые расширения gcc, не совместимые со стандартом, отключает в программах на языке С комментарии в стиле С++ (//) и включает обработку триграфов (трехсимвольных последовательностей) ANSI. Кроме того, она содержит макрос __STRICT_ANSI__, который отключает некоторые расширения в заголовочных файлах, не совместимые со стандартом. В последующих версиях компилятора принятый стандарт может измениться.

□ -std= — эта опция обеспечивает более тонкий контроль используемого стандарта, предоставляя параметр, точно задающий требуемый стандарт. Далее приведены основные возможные варианты:

 • с89 — поддерживать стандарт C89;

 • iso9899:1999— поддерживать последнюю версию стандарта ISO, C90;

 • gnu89 — поддерживать стандарт C89, но разрешить некоторые расширения GNU и некоторые функциональные возможности C99. В версии 4.2 gcc этот вариант применяется по умолчанию.

Опции для отслеживания стандарта в директивах define

Существуют константы (#defines), которые могут задаваться опциями в командной строке или виде определений в исходном тексте программы. Мы, как правило, считаем, что для них используется командная строка компилятора.

□ __STRICT_ANSI__ — заставляет применять стандарт С ISO. Определяется, когда в командной строке компилятора задана опция -ansi.

□ _POSIX_C_SOURCE=2 — активизирует функциональные возможности, определенные стандартами IEEE Std 1003.1 и 1003.2. Мы вернемся к этим стандартам чуть позже в этой главе.

□ _BSD_SOURCE — включает функциональные возможности систем BSD. Если они конфликтуют с определениями POSIX, определения BSD обладают более высоким приоритетом.

□ _GNU_SOURCE — допускает широкий диапазон свойств и функций, включая расширения GNU. Если эти определения конфликтуют с определениями POSIX, у последних более высокий приоритет.

Опции компилятора для вывода предупреждений

Эти опции передаются компилятору из командной строки. И снова мы перечислим лишь основные, полный список можно найти в интерактивном справочном руководстве gcc.

□ -pedantic — эта наиболее мощная опция проверки чистоты, программного кода на языке С. Помимо включения опции проверки на соответствие стандарту С, она отключает некоторые традиционные конструкции языка С, запрещенные стандартом, и делает недопустимыми все расширения GNU по отношению к стандарту. Эту опцию следует применять, чтобы добиться максимальной переносимости вашего кода на С. Недостаток ее в том, что компилятор сильно озабочен чистотой вашего программного кода, и порой приходится поломать голову для того, чтобы разделаться с несколькими оставшимися предупреждениями.

□ -Wformat — проверяет корректность типов аргументов функций семейства printf.

□ -Wparentheses — проверяет наличие скобок, даже там, где они не нужны. Эта опция очень полезна для проверки того, что сложные структуры инициализированы так, как задумано.

□ -Wswitch-default — проверяет наличие варианта default в операторах switch, что обычно считается хорошим стилем программирования.

□ -Wunused — проверяет разнообразные случаи, например, статические функции объявленные, но не описанные, неиспользуемые параметры, отброшенные результаты.

□ -Wall — включает большинство типов предупреждений gcc, в том числе все предыдущие опции -W (не охватывается только -pedantic). С ее помощью легко добиться чистоты программного кода.

Примечание

Существует еще огромное множество дополнительных опций предупреждений, все подробности см. на Web-страницах gcc. В основном мы рекомендуем применять -Wall; это удачный компромисс между проверкой, обеспечивающей программный код высокого качества, и необходимостью вывода компилятором массы тривиальных предупреждений, которые становится трудно свести к нулю.

Интерфейсы и Linux Standards Base

Теперь мы собираемся подняться на уровень выше и перейти от программного кода на языке С к рассмотрению интерфейсов (системных функций), предоставляемых операционной системой. У этого уровня стандартизации есть разные составляющие: функции, предоставляемые библиотеками, и системные вызовы, реализованные операционной системой на низком уровне. И у тех, и у других есть два уровня детализации: какие интерфейсы представлены и определение того, что делает каждый интерфейс.

Определяющий документ в этой области для ОС Linux — Linux Standards Base (LSB, стандарты операционных систем на базе Linux), который можно найти на Web-сайтах http://mvw.linuxbase.org или http://www.linux-foundation.org/en/LSB. Уже выпущено несколько версий стандартов, и работа продолжается.

Список дистрибутивов, прошедших сертификацию, можно найти по адресу http://www.linux-foundation.org/en/Products. Сертифицированы разные версии Red Hat, SUSE и Ubuntu, но помните о том, что после выпуска дистрибутива до момента сертификации должно пройти некоторое время. На Web-сайте есть список дистрибутивов, проходящих тестирование или только нуждающихся в некоторых обновлениях для того, чтобы пройти сертификационные испытания.

В стандарте Linux Standards Base (что касается версии 3.1) определены три области для проверки на соответствие:

□ ядро — основные библиотеки, утилиты и местонахождение ключевых компонентов файловой системы;

□ С++ — библиотеки С++;

□ рабочий стол — дополнительные файлы для установок рабочего стола, в основном разные графические библиотеки.

В спецификации нас интересует больше всего ядро.

Стандарт LSB охватывает ряд областей в собственной документации, но для определений конкретных интерфейсов также приводит ссылки на внешние стандарты. В стандарте описаны следующие области:

□ форматы объектных файлов для двоичной совместимости;

□ стандарты динамического связывания;

□ стандартные библиотеки, как базовые, так и библиотеки X Window System;

□ командная оболочка и другие программы режима командной строки;

□ среда исполнения, включая пользователей и группы;

□ инициализация системы и уровни запуска (run levels).

В этой главе мы обсудим только стандартные библиотеки, пользователей и инициализацию системы.

Стандартные библиотеки LSB

Документация Linux Standard Base определяет двумя способами интерфейсы, которые должны присутствовать. Для некоторых функций, в основном реализованных библиотекой С проекта GNU или склонных быть стандартами только для Linux, определяются и интерфейс, и его поведение. Для других интерфейсов, в особенности с UNIX-подобной основой, стандарт просто констатирует, что такой интерфейс должен присутствовать и должен вести себя, как определено другим стандартом, обычно Common Application Environment (CAE, общая прикладная среда) или еще чаще Single UNIX Specification (единая спецификация UNIX), который есть на Web-сайте Open Group http://www.opengroup.org. Некоторые части можно найти (в настоящее время требуется регистрация) по адресу http://www.unix.org/online.html.

К сожалению, у лежащих в основе стандартов для ОС Linux и UNIX-стандартов довольно запутанное прошлое, и существует слишком широкий выбор, хотя в основном разные версии почти совместимы.

Краткий урок истории

ОС UNIX родилась в конце 1960 гг. в подразделении Bell Laboratories компании AT&T, когда Кен Томпсон (Ken Thompson) и Деннис Ритчи (Dennis Ritchie) написали операционную систему, первоначально предназначенную только для личного пользования, которую назвали Unics. Каким-то образом имя изменилось на UNIX. AT&T разрешила университетам брать исходный программный код для собственных разработок, и система UNIX быстро стала невероятно популярной благодаря очень четкой логической структуре и мощным идеям. Наличие исходного программного кода должно было стать существенным стимулом, т. к. позволяло программистам вносить изменения и экспериментировать.

Операционная система BSD была вариантом, который появился благодаря работе, проделанной в Университете Калифорнии в Беркли, и уделившей много внимания организации и поддержке сети.

Когда компания AT&T начала превращать UNIX в коммерческую систему, что происходило главным образом в середине 1980 гг., она называла выпуски системы UNIX System, и самым популярным был UNIX System V.

Появилось много и других вариантов, слишком много, чтобы перечислять их здесь, все они имели небольшие отличия от базовых стандартов и некоторые дополнения, поскольку компании пытались повысить стоимость продукта, создавая собственные расширения.

Все по-настоящему усложнилось, когда AT&T продала UNIX-бизнес компании Novell, которая в 1994 г. решила его завершить, и владение правами и торговыми марками стало чем-то неопределенным, послужившим предметом разных судебных разбирательств.

В 1988 г. IEEE (Institute of Electrical and Electronic Engineers, Институт инженеров по электротехнике и радиоэлектронике, http://www.ieee.org) выпустил первый набор стандартов: POSIX или IEEE 1003 — стандартов, которые задумывались как определяющая спецификация переносимого интерфейса компьютерных операционных систем. Несмотря на то, что это хороший и четко определенный стандарт, POSIX — также во многом лишь спецификация ядра с очень ограниченной областью применения.

В 1994 г. X/Open Company, не участвующая в поставках организация, выпустила более полный набор спецификаций, X/Open CAE или Common Applications Environment (общая прикладная среда), представляющий собой расширенный вариант стандартов IEEE POSIX и формально идентичный им во многих областях. Компания X/Open позже объединилась с OSF (Free Software Foundation, фонд свободного программного обеспечения) для учреждения Open Group; ее исходная Web-страница находится по адресу http://www.opengroup.org/. Стандарт CAE был исправлен и выпущен в 2002 г. как Single UNIX Specification, Version 3 (единая спецификация UNIX, версия 3), разработанный Open Group.

Именно на эту спецификацию чаще всего ссылается база стандартов Linux.

Примечание

Следует отметить, что "Linux" — это торговая марка, принадлежащая Линусу Торвальдсу (Linus Torvalds). См. http://www.linuxmark.org/.

Применение стандарта LSB к библиотекам

Довольно об истории создания стандартов. Что означает для людей, пишущих программы на языке С (или С++), требование их переносимости?

Во-первых, вы должны убедиться в том, что используемая вами библиотечная функция приведена в стандарте LSB. Если ее там нет, возможно, вы делаете что-то, что нелегко будет перенести в другую систему, и вам следует поискать стандартный способ реализации той задачи, которую вы пытаетесь решить. Быть может, стоит попробовать команду Linux apropos, которая ищет страницы интерактивного справочного руководства для соответствующих ссылок.

Во-вторых, что труднее, следует убедиться в том, что поведение используемой вами функции включено в стандарт, и вы не полагаетесь на поведение, не описанное в стандарте. Возможно, для этого вам придется обратиться к стандарту Single UNIX Specification, если применение функции не определено в стандарте LSB. Очень хороший способ проверки неопределенного или потенциально ошибочного поведения — обращение к интерактивному руководству Linux. На многих его страницах есть раздел "BUGS" ("Ошибки"), представляющий собой неоценимый источник информации о том, где в ОС Linux конкретный вызов не в полной мере реализует стандарты или где существуют дефекты и нелепости в поведении.

Пользователи и группы LSB

Этот раздел стандарта точен, краток и понятен. Далее перечислены некоторые требования стандарта.

□ Спецификация требует для получения подробных сведений о пользователе никогда не читать напрямую такие файлы, как /etc/passwd, а всегда применять вызовы стандартной библиотеки, например getpwent, или стандартные утилиты, например passwd.

□ Стандарт требует наличия пользователя с именем root в группе root, который является администратором системы с полным набором привилегий или прав доступа. Мы также находим в стандарте ряд необязательных имен пользователей и групп, которые никогда не следует применять в стандартных приложениях; они предназначены для использования дистрибутивами.

□ В стандарте также указано, что ID, меньшие 100, — системные учетные записи, диапазон 100-499 занимают системные администраторы и постустановочные сценарии, и, наконец, ID с номерами 500 и большими предназначены для учетных записей обычных пользователей.

Как правило, большинство программистов Linux должно знать о требованиях стандартов, касающихся пользователей.

Инициализация системы LSB

Область инициализации или запуска системы всегда, по крайней мере для нас, была источником беспокойства из-за трудноуловимых различий дистрибутивов.

Система Linux унаследовала от UNIX-подобных операционных систем идею уровней запуска или выполнения, определяющих сервисы, постоянно выполняющиеся в системе. В табл. 18.1 приведены стандартные определения для ОС Linux.

Таблица 18.1

Уровень запуска Описание
0 Halt. Применяется как логическое состояние, к которому следует перейти при остановке системы
1 Однопользовательский режим. Каталоги, отличающиеся от / (корневой), могут не монтироваться, и сетевой поддержки не будет. Обычно применяется для обслуживания системы
2 Многопользовательский режим, но без сетевой поддержки
3 Обычный многопользовательский режим с сетевой поддержкой, использующий экран регистрации в текстовом режиме
4 Зарезервирован
5 Обычный многопользовательский режим с сетевой поддержкой, использующий экран регистрации в графическом режиме
6 Псевдоуровень, применяемый для перезагрузки

Стандарт LSB приводит эти уровни, но не требует их обязательного использования, хотя они и очень распространены.

Сопровождает уровни запуска набор сценариев инициализации, применяемых для запуска, останова и повторного запуска сервисов. В прошлом они хранились в разных местах в каталоге /etc, часто в /etc/init.d или в /etc/rc.d/init.d. Подобное разнообразие часто было причиной путаницы, поскольку пользователи, менявшие дистрибутивы, не могли найти сценарии инициализации в привычных местах, и установка программ завершалась аварийно при попытке выполнить сценарий инициализации из неверного каталога.

Стандарт LSB 3.1 определяет каталог /etc/init.d, как место хранения сценариев инициализации, но при этом разрешает этому каталогу быть ссылкой на другое место в системе.

У каждого сценария в каталоге /etc/init.d есть имя, связанное с предоставляемым им сервисом. Поскольку все сервисы ОС Linux должны совместно использовать одно пространство имен, важно, чтобы эти имена были уникальны. Например, жизнь будет несладкой, если сервисы MySQL и PostgreSQL решат назвать свои сценарии "database". Для устранения такого конфликта существует еще один набор стандартов. Это стандарт Assigned Names And Numbers Authority (LANANА, орган назначения имен и номеров в Linux), который можно найти на Web-сайте http://www.lanana.org/. К счастью, вам понадобится знать очень немногое об этом стандарте, за исключением того, что в нем хранится список зарегистрированных имен сценариев и пакетов, облегчающий жизнь пользователям систем Linux.

Сценарий инициализации должен принимать параметр, управляющий его действиями. В стандарте определены параметры, перечисленные в табл. 18.2.

Таблица 18.2

Параметр Значение
start Запускает (или перезапускает) сервис
stop Останавливает сервис
restart Перезапускает сервис; обычно реализован как простой останов сервиса, за которым следует запуск этого сервиса
reload Переустанавливает сервис, повторно загружая параметры без реальной остановки сервиса. Этот вариант поддерживают не все сервисы, поэтому данный параметр может быть недоступен в некоторых сценариях, а если доступен, то не имеет эффекта
force-reload Пытается вызвать переустановку, если сервис ее поддерживает, если нет — выполняет перезапуск сервиса
status Выводит текстовое сообщение о состоянии сервиса и возвращает код состояния, который может применяться для определения состояния сервиса

Все команды возвращают 0 в случае успешного завершения или код ошибки, обозначающий причину аварийного исхода. В случае параметра status возвращается 0, если сервис выполняется; все остальные коды означают, что сервис не запущен по какой-то причине.

Стандарт устройства файловой системы

Последний стандарт, который мы собираемся, рассмотреть в этой главе, — Filesystem Hierarchy Standard (FHS, стандарт иерархии файловой системы). Его можно найти по адресу http://www.pathname.com/fhs/.

Назначение этого стандарта — определение типовых мест хранения в файловой системе Linux для того, чтобы как разработчики, так и пользователи могли делать обоснованные предположения относительно местонахождения тех или иных файлов. Многолетние пользователи UNIX-подобных операционных систем долгое время жаловались на трудноуловимые различия в схемах расположения файловых систем, и стандарт FHS предлагает дистрибутивам Linux способ избежать повторения этого прерывистого пути.

Схема размещения файлов в системе Linux на первый взгляд может показаться полупроизвольной структурой файлов и каталогов, основанной на исторически сложившихся представлениях. Отчасти это правда, но с годами схема размещения небезосновательно эволюционировала в иерархию, которую мы видим сегодня. Основная ее идея — разделение файлов и каталогов на три следующие группы:

□ файлы и каталоги, уникальные для конкретной работающей системы Linux, такие как сценарии запуска и файлы конфигурации;

□ файлы и каталоги, предназначенные только для чтения и, возможно, совместно используемые несколькими работающими системами Linux, например исполняемые файлы приложений;

□ каталоги, предназначенные для чтения/записи, но, возможно, совместно используемые работающими системами Linux или другими операционными системами, например исходные каталоги пользователей.

В этой книге нас не слишком интересует совместное использование файлов разными версиями Linux, хотя, в случае сети из машин с ОС Linux, это отличный способ убедиться в том, что существует только одна копия каталогов ключевых программ, и совместно использовать ее на разных машинах в сети. Это особенно полезно для бездисковых рабочих станций.

В стандарте FHS определена структура верхнего уровня, имеющая ряд обязательных подкаталогов и несколько необязательных каталогов; основные из них приведены в табл. 18.3.

Таблица 18.3

Каталог Обязательный? Назначение
/bin Да Важные системные двоичные файлы
/boot Да Файлы, необходимые для загрузки системы
/dev Да Устройства
/etc Да Системные файлы конфигурации
/home Нет Каталоги для файлов пользователей
/lib Да Стандартные библиотеки
/media Да Место для съемных монтируемых носителей с отдельными подкаталогами для каждого типа носителей, поддерживаемого системой
/mnt Да Удобная точка для временно монтируемых устройств, таких как CD-ROM и накопители флэш-памяти
/opt Да Дополнительное прикладное программное обеспечение
/root Нет Файлы пользователя root
/sbin Да Важные системные двоичные файлы, которые необходимы в процессе запуска системы
/srv Да Предназначенные только для чтения данные для сервисов, предоставляемых данной системой
/tmp Да Временные файлы
/usr Да Вспомогательная иерархия. Традиционно файлы пользователей также хранятся здесь, но в наши дни это считается дурным стилем и обычным пользователем не следует предоставлять право записи в этот каталог
/var Да Переменные данные, например файлы регистрации

Кроме того, могут существовать и другие каталоги, начинающиеся с lib, хотя это и не распространено. Как правило, вы также будете встречать каталог /lost+found (для восстановления файловой системы с помощью программы fsck) и каталог /proc, представляющий собой псевдофайловую систему, обеспечивающую отображение работающей системы. Текущая версия стандарта FHS усиленно поддерживает файловую систему /proc, но ее присутствие не обязательно. Подробности, касающиеся системы /proc, в основном выходят за рамки тем, обсуждаемых в этой книге, хотя мы и привели ее краткий обзор в главе 3.

Далее познакомимся вкратце с назначением каждого из стандартных подкаталогов корневого каталога /.

□ /bin — содержит двоичные файлы, которые могут использовать как пользователь root, так и обычные пользователи и которые важны для функционирования в однопользовательском режиме, когда некоторые другие структуры каталогов могут не монтироваться. Например, обычно здесь можно найти команды ядра cat и ls, как и команду sh.

□ /boot — применяется для файлов, требуемых во время загрузки системы Linux. Часто этот каталог очень мал, менее 10 Мбайт, и часто это отдельный раздел. Это очень удобно в системах на базе PC, в которых есть ограничения BIOS для активного раздела, который должен находиться в первых 2 или 4 Гбайт диска. Имея этот каталог в виде отдельного раздела, вы будете обладать большей гибкостью при размещении остальных разделов диска.

□ /dev — содержит специальные файлы устройств, отображаемые на аппаратные устройства. Например, /dev/had будет отображаться на первый диск IDE.

□ /etc — содержит файлы конфигурации. По традиции здесь можно найти и некоторые двоичные файлы, но это уже не соответствует действительности для большинства современных систем Linux. Самый известный файл в каталоге /etc — это, вероятно, файл passwd, содержащий информацию о пользователях. Другие полезные файлы — fstab с перечнем вариантов монтирования; hosts со списком отображений IP-адресов в имена компьютеров, и каталог httpd, содержащий конфигурацию для сервера Apache.

□ /home — каталог для файлов пользователей. Обычно у каждого пользователя в этом каталоге есть один каталог с именем, совпадающим с регистрационным именем пользователя, и он будет регистрационным каталогом по умолчанию. Например, после регистрации пользователь rick почти наверняка обнаружит себя в каталоге /home/rick.

□ /lib — содержит важные совместно используемые библиотеки и модули ядра, особенно те, которые потребуются во время загрузки системы в однопользовательском режиме.

□ /media — задуман как каталог верхнего уровня для хранения каталогов-точек монтирования для съемных носителей. Цель — иметь возможность удалять ненужные каталоги верхнего уровня, такие как /cdrom и /floppy.

□ /mnt — просто удобное место для монтирования на время дополнительных файловых систем. По сложившейся традиции некоторые дистрибутивы добавляли в  каталог /mnt подкаталоги для разных устройств, таких как /cdrom и /floppy, но в настоящее время предпочтительнее размещать их в каталоге /media, вернув /mnt его первоначальное назначение — единое место размещения верхнего уровня для временного монтирования (single top-level temporary mount location).

□ /opt — каталог для поставщиков программного обеспечения, используемый для вставки программных приложений, добавляемых к базовому дистрибутиву. Дистрибутивы не должны пользоваться им для хранения программного обеспечения, которое поставляется как часть стандартного дистрибутива, его следует оставлять для использования сторонними поставщиками. Обычно поставщики будут создавать подкаталоги со своими именами и в них последующие каталоги, такие как /bin и /lib, для файлов, относящихся к их приложению.

Примечание

По принятому соглашению многие пакеты Open Source Linux используют каталог /usr/local для инсталляции.

□ /root — это каталог для файлов, используемых пользователем root. Он не входит в ветвь каталога /home в дереве каталогов, поскольку может не монтироваться в однопользовательском режиме.

□ /sbin — применяется для команд, обычно используемых только системным администратором и требующихся во время загрузки системы в однопользовательском режиме. Здесь обитают команды fsck, halt и swapon.

□ /srv — предназначен для размещения данных местного назначения в режиме "только для чтения", но в настоящее время он широко не используется.

□ /tmp — применяется для временных файлов. Обычно, но не всегда, очищается при загрузке системы.

□ /usr — довольно сложная вспомогательная файловая система, как правило, содержащая все команды системного типа и библиотеки, не требуемые при загрузке системы или в однопользовательском режиме. В каталоге много подкаталогов, таких как /bin, /lib, /X11R6 и /local.

Примечание

Когда только появились системы UNIX и Linux, каталог /usr также имел подкаталоги для регистраций, буферизации электронной почты и т.п. Теперь все эти подкаталоги удалены из каталога usr и помещены в каталог var. Преимущество такого подхода в том, что теперь /usr может быть монтируемой файловой системой, ее могут совместно использовать другие системы в сети, и он стал менее чувствителен к повреждениям системы, которые останавливают ее неуправляемым образом, например из-за отказа электропитания.

□ /var — содержит часто меняющиеся данные, такие как файлы буферов печати, файлы регистраций приложений и каталоги буферизации электронной почты.

Что еще почитать о стандартах?

Конечно, существует гораздо больше вещей, которые нужно знать, если вы хотите написать и применять полностью переносимое приложение Linux.

Вы хотите локализовать ваше приложение, так чтобы оно работало на разных языках и с разными региональными установками? Даже если вы ограничены английским языком, остается выбор валюты, разделителей в числах, форматов дат и множество других требующих внимания параметров. Как вы догадываетесь, есть специалисты, работающие над такими стандартами; увидеть их работу можно на Web-сайте http://www.openi18n.org/.

С другой стороны, нужно учитывать, какие параметры, версии библиотек и т.д. установлены в применяемой системе. К счастью, эта проблема становится менее острой во многом благодаря работе по стандартизации, о которой мы рассказали в этой главе, хотя и все еще может оказаться серьезной проблемой. Есть пара средств в проекте GNU, которые оказывают существенную помощь в решении данной проблемы: autoconf и automake. Хотя вы, возможно, не применяли их явно, почти наверняка вы видели пользу от их применения, когда устанавливали программное обеспечение из исходного программного кода и набирали ./configure; make.

Польза от применения этих средств выходит за рамки обсуждаемых в данной книге тем, но вы можете найти дополнительную информацию о них на Web-страницах проекта GNU http://www.gnu.org/software/autoconf/ и http://www.gnu.org/software/automake.

Резюме

В этой заключительной главе мы кратко рассмотрели некоторые из множества стандартов, помогающих сделать более легким программирование на платформе Linux и обеспечивающих соответствие разных дистрибутивов Linux некоторым базовым стандартам. Соответствие стандартам облегчает жизнь всем нам, программистам и пользователям, и мы настоятельно рекомендуем вам применять стандарты и поощрять их использование другими людьми.

Здесь и далее для удобства читателей комментарии переведены на русский язык. Возможность ввода знаков кириллицы зависит от выбранного дистрибутива Linux и текстового редактора. —