Text
                    Арнольд Роббинс
^
tel
¦
¦¦¦ )
1
¦" !
'¦¦ЧЬ* ¦
.¦nil
I 1
¦ :
Л
I
vJ|
4.
M
1
1
л
программирование
в примерах
КУДИЦ-ОБРАЗ
Москва ¦ 2005


ББК 32.973-018.2 Роббинс А. Linux: программирование в примерах. Пер с англ. - М.: КУДИЦ-ОБРАЗ, 2005. - 656 с. В книге рассмотрены вопросы, связанные с програмированием под Linux: файловый ввод/вывод, метаданьге файлов, основы управления памятью, процессы и сигналы, пользователи и группы, вопросы интернационализации и локализации, сортировка, поиск и многие другие. Много внимания уделено средствам отладки, доступным под GNU Linux. Все темы иллюстрируются примерами кода, взятого из V7 UNIX и GNU. Эта книга может быть полезна любому, кто интересуется програмированием под Linux. Арнольд Роббинс Linux: программирование в примерах Учебно-справочное издание Переводчик Р. Г. Галеев Научный редактор к. ф-м. н. И. В. Мурашко Корректор С. В. Красильникова . Макет В. Г. Клименко ООО «ИД КУДИЦ-ОБРАЗ» 119049, Москва, Ленинский проспект, д. 4, стр. 1А Тел.: 333-82-11, ok@kudits.ruhttp://books.kudits.ru Подписано в печать 14. L2.2004. Отпечатано в ОАО «Щербинская типография» Формат 70x90/16. Бум. газ. Печать офс. 117623, Москва, ул. Типографская, д. 10 Усл. печ. л. 46,8. Тираж 2000. Заказ 159 ISBN 5-9579-0059-1 (рус.) Перевод, макет, обложка © ООО «ИД КУДИЦ-ОБРАЗ», 2005. ISBN 0-13-142964-7 © 2004 Pearson Education, Inc Authorized translation from English language edition, entitled LINUX PROGRAMMING BY EXAMPLE, 1st Edition, ISBN: 0131429647, by ROBBINS, ARNOLD published by Pearson Education Inc, publishing as Prentice Hall PTR, Copyright © 2004 Pearson Education, Inc. All rights reserved. No part of this book may be reproduced or transmitted in any forms or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education Inc. RUSSIAN language edition published by KUDITS-OBRAZ, Copyright 2005. Авторизованный перевод с англоязычного издания, озаглавленного LINUX PROGRAMMING BY EXAMPLE, 1st Edition, ISBN: 0131429647, автор ROBBINS, ARNOLD, опубликованного Pearson Education, Inc, под издательской маркой Prentice Hall PTR, Copyright © 2004 Pearson Education, Inc. Все права защищены. Никакая часть этой книги не может воспроизводиться или распространяться в любой форме или любыми средствами, электронными или механическими, включая фотографирование, магнитную запись или информационно-поисковые системы хранения информации без разрешения от Pearson Education, Inc. Русское издание опубликовано издательством КУДИЦ-ОБРАЗ, © 2005.
Предисловие V-гдним из лучших способов научиться программированию является чтение хорошо написанных программ. Данная книга обучает фундаментальному API системных вызовов Linux - тех, которые образуют ядро любой значительной программы - представляя код из программных изделий, которые вы используете каждый день. Рассматривая конкретные программы, вы не только видите, как использовать Linux API, но можете также исследовать реальные проблемы (производительности, переносимости, устойчивости), которые возникают при написании программного обеспечения. Хотя книга называется Программирование под Linux на примерах, все, что мы рассматриваем, относится также к современным системам Unix, если не отмечено противное. Обычно мы используем термин «Linux» для обозначения ядра Linux, a «GNU/Linux» для обозначения всей системы (ядра, библиотек, инструментов). Часто также мы говорим «Linux», когда имеем в виду и Linux, GNU/Linux и Unix; если что-то является специфичным для той или иной системы, мы отмечаем это явным образом. Аудитория Данная книга предназначена для лиц, разбирающихся в программировании и знакомых С основами С, по крайней мере на уровне книги Кернигана и Ричи Программирование на языке С. (Программисты Java, желающие прочесть эту книгу, должны разбираться в указателях С, поскольку С активно их использует.) В примерах используются как версия Стандартного С 1990 года, так и Оригинальный С. В частности, вам следует быть знакомыми со всеми операторами С, структурами управления потоком исполнения, использованием объявлений переменных и указателей, функциями работы со строками, использованием exit () и набором функций <stdio .h> для файлового ввода/вывода. Вы должны понимать базовые концепции стандартного ввода, стандартного вывода и стандартной ошибки, а также знать тот факт, что все программы на С получают массив символьных строк, представляющих вызываемые опции и аргументы. Вы должны также быть знакомы с основными инструментами командной строки, такими, как cd, cp, date, In, Is, man (и info, если он у вас имеется), rmdir и rm, с использованием длинных и коротких опций командной строки, переменных окружения и перенаправления ввода/вывода, включая каналы. Мы предполагаем, что вы хотите писать программы, которые работают не только под GNU/Linux, но и на множестве различных систем Unix. С этой целью мы помечаем каждый интерфейс с точки зрения его доступности (лишь для систем GLIBC или определен в POSIX и т. д.), а в тексте приведены также советы по переносимости. Программирование, которое здесь приводится, может быть на более низком уровне, чем вы обычно использовали; это нормально. Системные вызовы являются основными строительными блоками для операций более высокого уровня и поэтому они низкоуровне-
6 Предисловие вые по своей природе. Это, в свою очередь, определяет использование нами С: функции API были спроектированы для использования из С, и код, связывающий их с языками более высокого уровня, такими как C++ и Java, неизбежно будет на более низком уровне и вероятнее всего, написанным на С. «Низкий уровень» не означает «плохой», это просто значит «более стимулирующий». Что вы изучите Данная книга фокусируется на базовых API, образующих ядро программирования под Linux: Управление памятью Файловый ввод/вывод Метаданные файлов Процессы и сигналы • Пользователи и группы • Поддержка программирования (сортировка, анализ аргументов и т. д.) • Интернационализация Отладка Мы намеренно сохранили список тем коротким. Мы считаем, что попытка научить в одной книге «всему, что можно узнать», пугает. Большинство читателей предпочитают книжки поменьше, более сфокусированные, и лучшие книги по Unix написаны таким способом. Поэтому вместо одного гигантского тома мы планируем несколько книг: одну по межпроцессному взаимодействию (IPC) и сетям, другую по разработке программного обеспечения и переносимости кода. Мы также положили глаз в направлении дополнительных томов в серии Программирование под Linux на примерах, которые будут раскрывать такие темы, как многопоточное программирование и программирование графических интерфейсов пользователя (GUI). Рассматриваемые нами API включают как системные вызовы, так и библиотечные функции. Действительно, на уровне С оба вида выступают в виде простых вызовов функций. Системный вызов является непосредственным запросом системной службы, такой, как чтение или запись файла или создание процесса. Библиотечная функция, с другой стороны, работает на уровне пользователя, возможно, никогда не запрашивая какие-либо сервисы у операционной системы. Системные вызовы документированы в разделе 2 справочного руководства (которое можно просмотреть с помощью команды man), а библиотечные функции документированы в разделе 3. Нашей целью является научить вас использовать Linux API на примерах: в частности, посредством использования, где это возможно, как оригинальных исходных кодов Unix, так и инструментов GNU. К сожалению, самодостаточных примеров не так много, как должно было бы быть. Поэтому мы также написали большое число небольших демонстра-
Небольшой - значит красивый: программы Unix 7 ционных программ. Был сделан акцент на принципах программирования: особенно на таких аспектах программирования для GNU, как «никаких произвольных ограничений», которые превращают инструменты GNU в незаурядные программы. Выбор для изучения повседневных программ намеренный. Если вы уже использовали GNU/Linux в течение какого-либо периода времени, вы уже понимаете, что делают такие программы, как Is и ср; после этого просто погрузиться прямо в то, как работают программы, не тратя много времени на изучение того, что они делают. Иногда мы представляем как высокоуровневый, так и низкоуровневый способы выполнения задачи. Обычно стандарт интерфейса более высокого уровня реализуется посредством более низкоуровневого интерфейса или конструкции. Мы надеемся, что такой взгляд на то, что происходит «под капотом», поможет вам понять, как это работает; для всего кода, который вы пишете сами, нужно всегда использовать более высокоуровневый, стандартный интерфейс. Таким же образом иногда мы представляем функции, которые предоставляют определенные возможности, а затем рекомендуем (по указанной причине) избегать этих функций! Главной причиной такого подхода является то, что вы получаете возможность узнавать эти функции при встрече и понимать код с их использованием. Всеобъемлющее знание темы требует понимания не только того, что вы можете сделать, но и того, что должны или не должны делать. Наконец, каждая глава завершается упражнениями. Некоторые из них требуют модификации или написания кода. Другие больше относятся к категориям «мысленных экспериментов» или «как вы думаете, почему...». Мы рекомендуем выполнить их все-они помогут закрепить понимание материала. Небольшой - значит красивый: программы Unix Закон Хоара: «Внутри каждой большой программы есть старающаяся пробиться маленькая программа» - C.A.R. Ноаге - Вначале мы планировали обучать Linux API, используя код инструментов GNU. Однако, современные версии даже простых программ командной строки (подобно mv и ср) большие и многофункциональные. Это особенно верно в отношении GNU вариантов стандартных утилит, которые допускают длинные и короткие опции, делают все, требуемое POSIX и часто имеют также дополнительные, внешне не связанные опции (подобно выделению вывода). Поэтому возник разумный вопрос: «Как мы можем в этом большом и запутывающем лесу сконцентрироваться на одном или двух важных деревьях?» Другими словами, если
8 Предисловие мы представим современные полнофункциональные программы, будет ли возможно увидеть лежащую в основе работу программы? Вот когда закон Хоара1 вдохновил нас на рассмотрение в качестве примера кода оригинальных программ Unix. Оригинальные утилиты V7 Unix маленькие и простые, что упрощает наблюдение происходящего и понимание использования системных вызовов. (V7 был выпущен около 1979 г.; это общий предок всех современных систем Unix, включая системы GNU/Linux и BSD.) В течение многих лет исходный код Unix был защищен авторскими правами и лицензионными соглашениями коммерческой тайны, что затрудняло его использование для обучения и делало невозможным опубликование. Это до сих пор верно в отношении исходного кода всех коммерческих систем Unix. Однако в 2002 г. Caldera (в настоящее время работающая под именем SCO) сделала оригинальный код Unix (вплоть до V7 и 32V Unix) доступным на условиях лицензии в стиле Open Source (см. приложение В «Лицензия Caldera для старой Unix»). Это дает нам возможность включить в эту книгу код из ранних систем Unix. Стандарты По всей книге мы ссылаемся на несколько различных официальных стандартов. Стандарт является документом, описывающим, как что-либо работает. Официальные стандарты существуют для многих веиДей, например, форма, размещение и назначение отверстий в электрической розетке на вашей стене определяется официальным стандартом, так что все сетевые шнуры в вашей стране работают со всеми розетками. Таким же образом официальные стандарты для вычислительных систем определяют, как они должны работать; это дает возможность пользователям и разработчикам знать, чего ожидать от своего программного обеспечения, и дает им возможность жаловаться своему поставщику, когда программное обеспечение не паботает. Здесь интерес для нас представляют: 1. ISO/IEC International Standard 9899: Programming Languages — С (Международный стандарт ISO/IEC 9899: Языки программирования - С), 1990. Первый официальный стандарт для языка программирования С. 2. ISO/IEC International Standard 9899: Programming Languages — С, Second edition, 1999 (Международный стандарт ISO/IEC 9899: Языки программирования - С, второе издание). Второй (текущий) официальный стандарт для языка программирования С. 3. ISO/IEC International Standard 14882: Programming Languages — C++, 1998 (Международный стандарт ISO/IEC 14882: Языки программирования - C++/ Первый официальный стандарт для языка программирования C++. Это знаменитое высказывание было сделано на Международном симпозиуме по эффективному производству больших программ в Jablonna, Польша, 10-14 августа 1970 г. - Примеч. автора.
Стандарты 9 4. ISO/1EC International Standard 14882: Programming Languages — C++, 2003 (Международный стандарт 14882: Языки программирования - C++). Второй (текущий) официальный стандарт для языка программирования C++. 5. IEEE Standard 1003.1-2001: Standard for Information Technology — Portable Operating System Interface (POSIX®) (Стандарт IEEE 1003.1-2001: Стандарт информационных технологий - переносимый интерфейс операционной системы). Текущая версия стандарта POSIX; описывает поведение, ожидаемое от Unix и Unix-подобных систем. Данное издание освещает как системные вызовы, так и библиотечные интерфейсы с точки зрения программиста C/C++, и интерфейс оболочки и инструментов с точки зрения пользователя. Он .состоит из нескольких томов: • Базовые определения (Base Definitions). Определения терминов, средств и заголовочных файлов. • Базовые определения - Обоснование (Base Definitions - Rationale). Объяснения и обоснования выбора средств как включенных, так и невключенных в стандарт. • Системные интерфейсы (System Interfaces). Системные вызовы и библиотечные функции. POSIX называет обе разновидности просто «функции». • Оболочка и инструменты (Shell and Utilities). Язык оболочки и доступные для интерактивного использования и использования сценариями оболочки инструменты. Хотя стандарты языков не являются захватывающим чтением, можно рассмотреть покупку экземпляра стандарта С: он дает окончательное определение языка. Книги можно приобрести в ANSI2 и в ISO3. (PDF-версия стандарта С вполне доступна.) Стандарт POSIX можно заказать в The Open Group4. Исследуя в каталоге их изданий элементы, перечисленные в «Спецификациях САЕ» («САЕ Specifications»), вы можете найти отдельные страницы для каждой части стандарта (озаглавленные с «С031» по «С034»). Каждая такая страница предоставляет свободный доступ к HTML версии определенного тома. Стандарт POSIX предназначен для реализации как Unix и Unix-подобных систем, так и не-Unix систем. Таким образом, базовые возможности, которые он предоставляет, составляют лишь часть возможностей, которые есть на системах Unix. Однако, стандарт POSIX определяет также расширения - дополнительные возможности, например, для многопо- точности или поддержки реального времени. 'Для нас важнее всего расширение Х/Ореп System Interface (XSI), описывающее возможности исторических систем Unix. По всей книге мы помечаем каждый API в отношении его доступности: ISO С, POSIX, XSI, только GLIBC или как нестандартный, но широко доступный. http: / /www. ans i . org - Примеч. автора. http : / /www. iso . ch - Примеч. автора. http : / /www. opengroup .org - Примеч. автора.
10 Предисловие Возможности и мощь: программы GNU Ограничив себя лишь оригинальным кодом Unix, можно было бы получить интересную историческую книгу, но она была бы не очень полезна в XXI веке. Современные программы не имеют тех же ограничений (памяти, мощности процессора, дискового пространства и скорости), которые были у ранних систем Unix. Более того, они должны работать в многоязычном мире - ASCII и американского английского недостаточно. Что еще важнее, одной из главных свобод, выдвинутых явным образом Фондом бесплатных программ (Free Software Foundation) и проектом GNU5, является «свобода обучения». Программы GNU предназначены для обеспечения большого собрания хорошо написанных программ, которые программисты среднего уровня могут использовать в качестве источника для своего обучения. Используя программы GNU, мы преследуем две цели: показать вам хорошо написанный современный код, на котором вы будете учиться писать хорошие программы, а также использовать API. Мы считаем, что программное обеспечение GNU лучше, поскольку оно свободно (в смысле «свободы», а не «бесплатного пива»N. Но признается также, что программное обеспечение GNU часто также технически лучше соответствующих двойников в Unix, и мы уделили место в разделе 1.4 «Почему программы GNU лучше», чтобы это объяснить. Часть примеров кода GNU происходит из gawk (GNU awk). Главной причиной этого является то, что это программа, с которой.мы очень знакомы, поэтому было просто отобрать оттуда примеры. У нас нет относительно нее других притязаний. Обзор глав Вождение автомобиля целостный процесс, включающий множество одновременных задач. Во многих отношениях программирование по Linux похоже на это, требуя понимания многих аспектов API, таких, как файловый ввод/вывод, метаданные файлов, каталоги, хранение сведений о времени и т. д. В первой части книги рассматривается достаточное количество этих отдельных элементов, чтобы дать возможность изучить первую значительную программу, V7 Is. Затем мы завершим наше обсуждение файлов и пользователей, рассмотрев иерархии файлов и принципы работы файловых систем и их использование. Глава 1, «Введение», описывает модели файлов и процессов Unix и Linux, рассматривает отличия оригинального С от стандартного С 1990 г., а также предоставляет обзор принципов, которые делают программы GNU в целом лучшими по сравнению со стандартными программами Unix. http: / /www. gnu. org - Примеч. автора. 6 Игра слов: free - свободный, бесплатный. - Примеч. перев.
Обзор глав 11 Глава 2, «Аргументы, опции и переменные окружения»; :'< П :i описывает, как С программа получает доступ к аргументам и опциям командной строки и обрабатывает их, а также объясняет, как работать с переменными окружения. Глава 3, «Управление памятью на уровне пользователя», предоставляет обзор различных видов используемой памяти, доступной работающему процессу. Управление памятью на уровне пользователя является основным для каждого нетривиального приложения, поэтому важно понять это с самого начала. Глава 4, «Файлы и файловый ввод/вывод», обсуждает базовый файловый ввод/вывод, показывая, как создавать и использовать файлы. Понимание этого важно для всего последующего материала. Глава 5, «Каталоги и слуэюебные данные файлов», описывает, как работают каталоги, прямые и символические ссылки. Затем описываются служебные данные файлов, такие, как владелец, права доступа и т. д., а также освещается работа с каталогами. Глава 6, «Общие библиотечные интерфейсы - часть 1», рассматривает первый набор интерфейсов общего программирования, которые будут нам нужны для эффективного использования служебных данных файлов. Глава 7, «Соединяя все вместе: is», связывает воедино все рассмотренное до сих пор, рассматривая программу V7 Is. Глава 8, «Файловые системы и обходы каталогов», описывает, как монтируются и демонтируются файловые системы и как программа может получить сведения о том, что смонтировано в системе. В главе описывается также, как программа может легко «обойти» всю иерархию файлов, предпринимая в отношении каждого встреченного объекта необходимые действия. Вторая часть книги имеет дело с созданием и управлением процессами, межпроцессным взаимодействием посредством каналов и сигналов, ГО пользователей и групп и дополнительными интерфейсами общего программирования. Далее в книге сначала описываются интернационализация с использованием GNU gettext, а затем несколько расширенных API. Глава 9, «Управление процессами и каналы», рассматривает создание процесса, исполнение программы, межпроцессное взаимодействие (IPC) с использованием каналов и управление дескрипторами файлов, включая неблокирующий ввод/вывод. Глава 10, «Сигналы», обсуждает сигналы, упрощенную форму межпроцессного взаимодействия. Сигналы играют также важную роль в управлении родительским процессом порожденных процессов.
12 Предисловие Глава 77, «Права доступа и ID пользователей и групп», ". * рассматривает, как идентифицируются процессы,и файлы, как работает проверка прав доступа и как работают механизмы setuid и setgid. Глава 12, «Общие библиотечные интерфейсы - часть 2», рассматривает оставшуюся часть общих API; многие из них более специализированы, чем первый общий набор API. Глава 13, «Интернационализация и локализация», объясняет, как почти без усилий обеспечить работу ваших программ на нескольких языках. Глава 14, «Расширенные интерфейсы», описывает несколько расширенных версий интерфейсов, освещенных в предыдущих главах, а также более подробно освещает блокировку файлов. Мы завершаем книгу главой об отладке, поскольку (почти) никто не может сделать все правильно с первого раза, и рекомендуем финальный проект для закрепления ваших знаний по рассмотренным в данной книге API. Глава 15, «Отладка», описывает основы отладчика GDB, передает как можно больше нашего опыта в этой об- ласти и рассматривает несколько полезных инструментов для осуществления различных видов отладки. Глава 16, «Проект, связывающий все воедино», представляет значительный проект по программированию, который использует почти все, рассмотренное в книге. Несколько приложений освещают представляющие интерес темы, включая лицензии для использованного в данной книге исходного кода. Приложение А, «Научитесь программированию за десять лет», ссылается на знаменитое высказывание: «Москва не сразу строилась»7. Также и квалификация в Linux/Uftix и понимание этих систем приходит лишь со временем и практикой. С этой целью мы включили это эссе Петера Норвига, которое мы горячо рекомендуем. Прилоэюение Д «Лицензия Caldera для старой Unix», охватывает исходный код Unix, использованный в данной книге. Приложение С, «Общедоступная лицензия GNU», охватывает исходный код GNU, использованный в данной книге. В оригинале: «Rome wasn't built in a day». - Примеч. перев.
Соглашения об обозначениях 13 Соглашения об обозначениях Как и в любой книге на компьютерную тему, для передачи информации мы используем определенные соглашения об обозначениях. Определения или первое использование термина выделяется курсивом, как слово «Определения» в начале этого предложения. Курсив используется также для выделения, для цитирования других работ и комментариев в примерах. Переменные элементы, такие, как аргументы или имена файлов, выглядят таким образом. Иногда мы используем жирный шрифт, когда нужно усилить мысль. Вещи, находящиеся на компьютере, обозначаются моноширинными шрифтом, как в случае имен файлов (foo.c) и названий команд (Is, grep). Короткие фрагменты, которые вы вводите, дополнительно заключаются в одинарные кавычки: 4 s -1 *.с\ $ и > являются первичным и вторичным приглашениями оболочки Борна и используются при отображении интерактивных примеров. Ввод пользователя выделяется другим шрифтом от обычного вывода компьютера в примерах. Примеры выглядят следующим образом: $ Is -1 /* Просмотр файлов. Опция - цифра 1, а не буква 1 */ foo bar baz Мы предпочитаем оболочку Борна и ее варианты (ksh93, Bash) по сравнению с оболочкой С; соответственно на всех наших примерах показана лишь оболочка Борна. Знайте, что правила применения кавычек и переноса на следующую строку в оболочке С другие; если вы используете ее, то на свой страх и риск!8 При ссылках на функции в программах мы добавляем к имени функции пустую пару скобок: print f (), strcpy (). При ссылке на справочную страницу (доступную по команде man), мы следуем стандартному соглашению Unix по написанию имени команды или функции курсивом, а раздела- в скобках после имени обычным шрифтом: awk(l), printfl})9. Где получить исходные коды Unix и GNU Вы можете захотеть получить копии программ, которые мы использовали в данной книге, для своих собственных экспериментов и просмотра. Весь исходный код доступен через Интернет, а ваш дистрибутив GNU/Linux содержит исходный код для инструментов GNU. См. справочные страницы csh(\) и tcsh(\) и книгу Using csh & tcsh, by Paul DuBois, O'Reilly & Associates, Sebastopol, CA, USA, 1995. ISBN: 1-56592-132-1. - Примеч. автора. Соответствующие справочные страницы можно посмотреть с помощью man 1 awk и man 3 printf. - Примеч. науч. ред.
14 Предисловие Код Unix Архивы различных «древних» версий Unix поддерживаются Обществом наследства Ш1Х (The UNIX Heritage Society -TUHS), http: / /www. tuhs . org. Наибольший интерес представляет возможность просматривать архив старых исходных кодов Unix через веб. Начните с http:7/minnie.tuhs.org/UnixTree/. Все примеры кода в данной книге из седьмого издания исследовательской системы UNIX, известной также как «V7». Сайт TUHS физически расположен в Австралии, хотя имеются зеркала архива по всему миру - см. http://www.tuhs.org/archive__sites.html. Эта страница также указывает, что архив доступен для зеркала через rsync. (Если у вас нет rsync, см. http: / /rsync . samba. org/: это стандартная утилита на системах GNU/Linux.) Чтобы скопировать весь архив, потребуется примерно 2-3 гигабайта дискового пространства. Для копирования архива создайте пустой каталог, а в нем выполните следующие команды: mkdir Applications 4BSD PDP-11 PDP-11/Trees VAX Other rsync -avz minnie.tuhs.org::UA_Root . . rsync -avz minnie.tuhs.org::UA__Applications Applications rsync -avz minnie.tuhs.org::UA_4BSD 4BSD rsync -avz minnie. tuhs. org: :UA_lPDP 11 PDP-11 rsync -avz minnie.tuhs.org::UA_PDPll_Trees PDP-11/Trees rsync -avz minnie.tuhs.org::UA_VAX VAX rsync -avz minnie.tuhs.org::UA_0ther Other Вы можете пропустить копирование каталога Trees, который содержит извлечения из нескольких версий Unix и занимает на диске около 700 мегабайт. В рассылке TUHS можно также поинтересоваться, нет ли поблизости от вас кого- нибудь, кто мог бы предоставить вам архив на CD-ROM, чтобы избежать пересылки по Интернету такого большого количества данных. Группа в Southern Storm Software, Pty. Ltd. в Австралии «модернизировала» часть кода уровня пользователя V7, так что его можно откомпилировать и запустить на современных системах, особенно на GNU/Linux. Этот код можно загрузить с их веб-сайта10. Интересно отметить, что код V7 не содержит в себе каких-либо уведомлений об авторских правах или разрешениях. Авторы писали код главным образом для себя и своего исследования, оставив проблемы разрешений отделу корпоративного лицензирования AT&T. http: / /www. southern-storm. com. au/v7upgrade. html - Примеч. автора.
Где получить исходные коды Unix и GNU 15 Код GNU Если вы используете GNU/Linux, ваш дистрибутив поступит с исходным кодом, предположительно в формате, используемом для упаковки (файлы RPM Red Hat, файлы DEB Debian, файлы . tar. gz Slackware и т. д.) Многие примеры в книге взяты из GNU Coreutils, версия 5.0. Найдите соответствующий CD-ROM для своего дистрибутива GNU/ Linux и используйте для извлечения кода соответствующий инструмент. Или следуйте для получения кода инструкциям в следующих нескольких абзацах. Если вы предпочитаете самостоятельно получать файлы из ftp-сайта GNU, вы найдете его по адресу: ftp: //ftp.gnu.org/gnu/coreutils/coreutils-5 . О . tar.gz. Для получения файла можно использовать утилиту wget: $ wget ftp: //ftp.gnu.org/gna/cx3reutil8/ooreutils-5.0 .tar.gz /* Получить дистрибутив */ /*.... здесь при получении файла куча вывода ... */ В качестве альтернативы можно использовать для получения файла старый добрый ftp: $'ftp ftp.gnu.org /* Подключиться к ftp-сайту GNU */ Connected to ftp.gnu.org A99.232.41.7). 220 GNU FTP server ready. Name (ftp.gnu.org:arnold): anonymous /* Использовать анонимный ftp */ 3 31 Please specify the password. Password: /* Пароль на экране не отображается */ 230~If you have any problems with the GNU software or its downloading, 230-please refer your questions to <gnu@gnu.org>. ... /* Куча вывода опущена */ 230 Login successful. Have fun. Remote system type is UNIX. Using binary mode to transfer files. ftp> cd /gnu/coreutils /* Перейти в каталог Coreutils */ 250 Directory successfully changed. ftp> bin 200 Switching to Binary mode. ftp> hash /* Выводить символы # по мере закачки */ Hash mark printing on A024 bytes/hash mark). ftp> get coreutils-5.0.tar.gz Retrieve file local: coreutils-5.0.tar.gz remote: coreutils-5.0.tar.gz 227 Entering Passive Mode A99,232,41,7,86,107) 150 Opening BINARY mode data connection for coreutils-5.0.tar.gz F020616 bytes) ###################################################################### ###################################################################### 226 File send OK. 6020616 bytes received in 2.03e+03 sees B.§ Kbytes/sec) ftp> quit /* Закончить работу */ 221 Goodbye.
16 Предисловие Получив файл, извлеките его следующим образом: $ gzip -de < coreutils-5.0. tar.gz | tar *-xvpf - /* Извлечь файлы */ /* ... при извлечении файла куча вывода ... */ Системы, использующие GNU tar, могут использовать следующее заклинание: $ tar -xvpzf coreutils-5.0.tar.gz /* Извлечь файлы */ /* ... при извлечении файла куча вывода ... */ В соответствии с общедоступной лицензией GNU, вот сведения об авторских правах для всех GNU программ, процитированных в данной книге. Все программы являются «свободным программным обеспечением; вы можете распространять их и/или модифицировать на условиях общедоступной лицензии GNU в изданном Фондом бесплатных программ виде; либо версии 2 лицензии, либо (по вашему выбору) любой последующей версии». Текст общедоступной лицензии GNU см. в приложении С «Общедоступная лицензия GNU». Файл Coreutils 5.0 lib/safe-read.с lib/safe-write.с lib/utime.c lib/xreadlink.с src/du.c sre/env.с src/install.с src/link.с src/ls.с srWpathchk.c sre/sort.с src/sys2.h sre/wc.с Файл Gawk 3.0.6 eval.с Файл Gawk 3.1.3 awk.h builtin.c eval.с io. с Даты авторского права © 1993-1994, 1998,2002 © 2002 © 1998,2001-2002 © 2001 © 1988-1991, 1995-2003 © 1986, 1991-2003 © 1989-1991, 1995-2002 ©2001-2002 © 1985, 1988, 1990, 1991, 1995-2003 © 1991-2003 © 1988, 1991-2002 © 1997-2003 © 1985, 1991, 1995-2002 Даты авторского права © 1986, 1988, 1989, 1991-2000 Даты авторского права © 1986, 1988, 1989, 1991-2003 © 1986, 1988, 1989, 1991-2003 © 1986, 1988, 1989, 1991-2003 © 1986, 1988, 1989, 1991-2003
Где получить примеры программ, использованные в данной книге 17 main, с © 1986, 1988, 1989, 1991-2003 posix/gawkmisc . с © 1986, 1988, 1989, 1991-1998, 2001-2003 Файл Gawk 3.1.4 Даты авторского права builtin.c ©1986,1988,1989,1991-2004 Файл GLIBC 2.3.2 Даты авторского права locale/ locale, h © 1991, 1992, 1995-2002 posix/unistd.h © 1991-2003 time/sys/time.h © 1991-1994, 1996-2003 Файл Make 3.80 Даты авторского права read, с ©1988-1997,2002 Где получить примеры программ, использованные в данной книге Примеры программ, использованные в данной книге, можно найти по адресу: http: / /authors.phptr.com/robbins. 06 обложке «Это оружие Джедая ..., элегантное оружие для более цивилизованной эпохи. На протяжении тысяч поколений Рыцари Джедай были защитниками мира и справедливости в Старой Республике. От мрачных времен, до Империи». - Оби-Ван Кеноби- Возможно, вы удивляетесь, почему мы поместили на обложке легкую саблю и использовали ее во внутреннем оформлении книги. Что она представляет и какое она имеет отношение к программированию под Linux? В руках Рыцаря Джедай легкая сабля является и мощным оружием, и предметом красоты. Ее использование демонстрирует мощь, знание, контроль над Силой и напряженное обучение для владеющего им Джедая. Элегантность легкой сабли отражает элегантность оригинального дизайна Unix API. Там также обдуманное, точное использование API и программных инструментов и принципов проектирования GNU привело к сегодняшним мощным, гибким, развитым системам GNU/Linux. Эта система демонстрирует знание и понимание программистов, создавших все их компоненты. И конечно, легкие сабли - это просто круто!
18 Предисловие Благодарности Написание книги требует большого количества работы, а чтобы сделать это хорошо, нужна помощь от многих людей. Д-р Brian W. Kernighan, д-р Doug Mcllroy, Peter Memishian и Peter van der Linden сделали рецензию первоначального предложения. David J. Agans, Fred Fish, Don Marti, Jim Meyering, Peter Norvig и Julian Seward достали разрешения на воспроизведение различных элементов, процитированных по всей книге. Спасибо Geoff Collyer, Ulrich Drepper, Yosef Gold, д-ру C.A.R. (Tony) Hoare, д-ру Manny Lehman, Jim Meyering, д-ру Dennis M. Ritchie, Julian Seward, Henry Spencer и д-ру Wladyslaw M. Turski за предоставление множества полезной общей информации. Спасибо также другим членам группы GNITS: Karl Berry, Akim DeMaille, Ulrich Drepper, Greg McGary, Jim Meyering, Francois Pinard и Tom Tromey, которые предоставили полезную обратную связь относительно хорошей практики программирования. Karl Berry, Alper Ersoy и д-р Nelson H.F. Beebe предоставили ценную техническую помощь по Texinfo и DocBook/XML. Хорошие технические обзоры не только гарантируют, что автор использует правильные факты, они также гарантируют, что он тщательно обдумывает свое представление. Д-р Nelson H.F. Beebe, Geoff Collyer, Russ Cox, Ulrich Drepper, Randy Lechlitner, д-р Brian W. Kernighan, Peter Memishian, Jim Meyering, Chet Ramey и Louis Taber работали в качестве технических рецензентов для всей книги. Д-р Michael Brennan предоставил полезные комментарии для главы 15. Их рецензии принесли пользу как содержанию, так и многим примерам программ. Настоящим я благодарю их всех. Как обычно говорят в таких случаях большинство авторов, «все оставшиеся ошибки мои». Я особенно хотел бы поблагодарить Mark Taub из Pearson Education за инициирование этого проекта, за его энтузиазм Для этой серии и за его помощь и советы по мере прохождения книги через различные ее стадии. Anthony Gemmellaro сделал феноменальную работу по реализации моей идеи для обложки, а внутренний дизайн Gail Cocker великолепен. Faye Gemmellaro сделал процесс производства вместо рутины приятным. Dmitry Kirsanov и Alina Kirsanova сделали рисунки, макеты страниц и предметный указатель; работать с ними было одно удовольствие. Наконец, моя глубочайшая благодарность жене Мириам за ее поддержку и ободрение во время написания книги. Арнольд Роббинс NofAyalon ИЗРАИЛЬ
Часть 1 Файлы и пользователи Глава 1. Введение - - 20 Глава 2. Аргументы, опции и переменные окружения .--.-- 37 Глава 3. Управление памятью на уровне пользователя 63 Глава 4. Файлы и файловый ввод/вывод 91 Глава 5. Каталоги и служебные данные файлов - -123 Глава &. Общие библиотечные интерфейсы - часть 1 167 Глава 7. Соединяя все вместе: Is 205 Глава 8. Файловые системы и обходы каталогов - - - 224
Глава 1 Введение В этой главе: 1.1. Модель файловой системы Linux/Unix 20 1.2. Модель процессов Linux/Unix . .. . .25 1.3. Стандартный С против оригинального С ?........... ,27 1.4. Почему программы GNU лучше 30 1.5. Пересмотренная переносимость , 34 1.6. Рекомендуемая литература 35 1.7. Резюме 36 И/сли есть одна фраза, резюмирующая важнейшие понятия GNU/Linux (а следовательно, и Unix), это «файлы и процессы». В данной главе мы рассмотрим модели файлов и процессов в Linux. Их важно понять, потому что почти все системные вызовы имеют отношение к изменению какого-либо атрибута или части состояния файла или процесса. Далее, поскольку мы будем изучать код в обеих стилях, мы кратко рассмотрим главные различия между стандартным С 1990 г. и первоначальным С. Наконец, мы довольно подробно обсудим то, что делает GNU-программы «лучше» - принципы программирования, использование которых в коде мы увидим. В данной главе содержится ряд умышленных упрощений. Детали в подробностях будут освещены по мере продвижения по книге. Если вы уже являетесь специалистом в Linux, пожалуйста, простите нас. 1.1. Модель файловой системы Linux/Unix Одной из движущих целей первоначального проекта Unix была простота. Простые понятия легко изучать и использовать. Когда понятия переведены в простые API, легко проектировать, писать и отлаживать простые программы. Вдобавок, простой код часто занимает меньше места и он более эффективен, чем более усложненные проекты. Поиск простоты направлялся двумя факторами. С технической точки зрения, первоначальные миникомпьютеры PDP-11, на которых разрабатывалась Unix, имели маленькое адресное пространство: 64 килобайта на меньших системах, 64 Кб кода и 64 Кб данных на больших. Эти ограничения относились не только к обычным программам (так называемому коду уровня пользователя), но и к самой операционной системе (коду уровня ядра). Поэтому не только «Маленький - значит красивый» в эстетическом смысле, но «Маленький - значит красивый», потому что не было другого выбора!
1.1. Модель файловой системы Linux/Unix 21 Вторым фактором была отрицательная реакция на современные коммерческие операционные системы, которые были без надобности усложнены, со сложными командными языками, множеством разновидностей файлового ввода-вывода и слабой общностью или гармонией. (Стив Джонсон однажды заметил: «Использование TSO подрбно линанию мертвого кита на побережье», TSO - это как раз одна из только что описанных бестолковых систем с разделением времени .для мэйнфреймов.) 1.1.1. Файлы и права доступа Файловая модель Unix проста, как фраза: файл - это линейный поток байтов. Точка. Операционная система не накладывает на файлы никаких предопределенных структур; ни фиксированных или переменных размеров записей, ни индексированных файлов, ничего. Интерпретация содержимого файла целиком оставлена приложению. (Это не совсем верно, как мы вскоре увидим, но для начала достаточно близко к истине.) Если у вас есть файл, вы можете сделать с данными в файле три вещи: прочитать, записать или исполнить их. Unix разрабатывался для миникомпьютеров с разделением времени; это предполагает наличие с самого начала многопользовательского окружения. Раз есть множество пользователей, должно быть возможным указание прав доступа к файлам: возможно, пользователь jane является начальником пользователя fred, и jane не хочет, чтобы fred прочел последние результаты аттестации. В целях создания прав доступа пользователи подразделяются на три различные категории: владелец файла; группа пользователей, связанная с данным файлом (вскоре будет пояснено); и остальные пользователи. Для каждой из этих категорий каждый файл имеет отдельные, связанные с этим файлом, биты прав доступа, разрешающие чтение, запись и исполнение. Эти разрешения отображаются в первом поле вывода команды 'Is -Г: , $ 1* -I progex.texi -rw-r—г— 1 arnold devel 5614 F@b 24 18:02 progex,texi Здесь arnold и devel являются соответственно владельцем и группой файла progex.texi, a -rw-r—r— является строкой типа файла и прав доступа. Для обычного файла первым символом будет дефис, для каталогов - d, а для других видов файлов - небольшой набор других символов, которые пока не имеют значения. Каждая последующая тройка символов представляют права на чтение, запись и исполнение для владельца, группы и «остальных» соответственно. В данном примере файл progex, texi может читать и записывать владелец файла, а группа и остальные пользователи могут только читать. Дефисы означают отсутствие разрешений, поэтому этот файл никто не может исполнить, а группа и остальные пользователи не могут в него записывать. Владелец и группа файла хранятся в виде числовых значений, известных как идентификаторпользователя (user ID - UID) и идентификатор группы (group ID ~ GID); стандартные библиотечные функции, которые мы рассмотрим далее в книге, позволяют напечатать эти значения в виде читаемых имен.
22 Глава 1. Введение Владелец файла может изменить разрешения, используя команду chmod (change mode - изменить режим). (Права доступа к файлу, по существу, иногда называют «режимом файла».) Группу файла можно изменить с помощью команд chgrp (change group - изменить группу) и chown (change owner - сменить владельцаI. Групповые права доступа были нацелены на поддержку совместной работы: хотя определенным файлом может владеть один член группы или подразделения, возможно, каждый член группы должен иметь возможность изменять его. (Рассмотрите совместный маркетинговый доклад или данные исследования.) Когда система проверяет доступ к файлу (обычно при открытии файла), если UID процесса совпадает с UID файла, используются права доступа владельца файла. Если эти права доступа запрещают операцию (скажем, попытка записи в файл с доступом -r--rw-rw-), операция завершается неудачей; Unix и Linux не продолжают проверку прав доступа для группы и других пользователей2. Это верно также, если UID различаются, но совпадают GID; если права доступа группы запр»ещают операцию, она завершается неудачей. Unix и Linux поддерживают понятие суперпользователя (superuser): это пользователь с особыми привилегиями. Этот пользователь известен как root и имеет UID, равный 0. root позволено делать все; никаких проверок, все двери открыты, все ящики отперты3. (Это может иметь важные последствия для безопасности, которых мы будем касаться по всей книге, но не будем освещать исчерпывающе.) Поэтому, даже если файл имеет режим , root все равно может читать файл и записывать в него. (Исключением является то, что файл нельзя исполнить. Но поскольку root может добавить право на исполнение, это ограничение ничего не предотвращает.) Модель прав доступа владелец/группа/другие, чтение/запись/исполнение проста, тем не менее достаточно гибка, чтобы охватывать большинство ситуаций. Существуют другие, более мощные, но и более сложные модели, реализованные на других системах, но ни одна из них не стандартизирована достаточно хорошо и не реализована достаточно широко, чтобы заслуживать обсуждения в общем руководстве, подобном этому. 1.1.2. Каталоги и имена файлов Раз у вас есть файл, нужно где-то его хранить. В этом назначение каталога (известного в системах Windows или Apple Macintosh под названием «папка»). Каталог является особой разновидностью файла, связывающего имена файлов с метаданными, известными как узлы (inodes). Каталоги являются особыми, поскольку их может обновлять лишь операционная система путем описанных в главе 4, «Файлы и файловый ввод-вывод», сис- 1 Некоторые системы позволяют рядовым пользователям назначать владельцем их файла кого-нибудь еще, таким образом «отдавая его». Детали определяются стандартом POSIX, но они несколько запутаны. Обычная конфигурация GNU/Linux не допускает этого. - Примеч. автора. 2 Конечно, владелец всегда может изменить права доступа. Большинство пользователей не отменяют для себя права на запись. - Примеч. автора. 3 Для этого правила есть несколько редких исключений, но все они выходят за рамки данной книги. - Примеч. автора
1.1. Модель файловой системы Linux/Unix 23 темных вызовов. Они особые также потому, что операционная система предписывает формат элементов каталога. Имена файлов могут содержать любой 8-битный байт, за исключением символа V (прямой косой черты) и ASCII символа NUL, все биты которого содержат 0. Ранние Unix- системы ограничивали имена 14 байтами; современные системы допускают отдельные имена файлов вплоть до 255 байтов. Узел содержит всю информацию о файле, за исключением его имени: тип, владелец, группа, права допуска, размер, времена изменения и доступа. Он хранит также размещение на диске блоков, содержащих данные файла. Все это данные о файле, а не данные самого файла, отсюда термин метаданные. Права доступа к каталогам пб сравнению с правами доступа к файлам имеют несколько другой смысл. Разрешение на чтение означает возможность поиска в каталоге; т. е. его просмотр с целью определить, какие файлы в нем содержатся. Разрешение на запись дает возможность создавать и удалять файлы в каталоге. Разрешение на исполнение означает возможность прохода через каталог при открытии или ином доступе к содержащемуся файлу или подкаталогу. ^ЗАМЕЧАНИЕ. Если у вас есть разрешение на запись в каталог, вы можете удалять I файлы из этого каталога, даже если они не принадлежат вам! При интерактивной Iработе команда rm отмечает это, запрашивая в таком случае подтверждение. I Каталог / tmp имеет разрешение на запись для каждого, но ваши файлы в I tmp {находятся вполне в безопасности, поскольку /tmp обычно имеет установленный так {называемый «липкий» (sticky) бит: 1$ Is -Id /tmp Idrwxrwxrwt 11 root root 4096 May 15 17:11 /tmp [Обратите внимание, что t находится в последней позиции первого поля. В большинстве каталогов в этом месте стоит х. При установленном «липком» бите ваши {файлы можете удалять лишь вы, как владелец файла, или root. (Более детально \это обсуэ/сдается в разделе 11.2.5, «Каталоги и липкий бит».) 1.1.3. Исполняемые файлы Помните, мы говорили, что операционная система на накладывает структуру на файлы? Мы уже видели, что это было невинной ложью относительно каталогов. Это же относится к двоичным исполняемым файлам. Чтобы запустить программу, ядро должно знать, какая часть файла представляет инструкции (код), а какая - данные. Это ведет к понятию формата объектного файла, которое определяет, как эти данные располагаются внутри файла на диске! Хотя ядро запустит лишь файлы, имеющие соответствующий формат, создание таких файлов задача утилит режима пользователя. Компилятор с языка программирования (такого как Ada, Fortran, С или C++) создает объектные файлы, а затем компоновщик или загрузчик (обычно с именем Id) связывает объектные файлы с библиотечными функциями для окончательного создания исполняемого файла. Обратите внимание, что даже если все
24 Глава 1. Введение нужные биты в файле размещены в нужных местах, ядро не запустит его, если не установлен соответствующий бит, разрешающий исполнение (или хотя бы один исполняющий бит для root). Поскольку компилятор, ассемблер и загрузчик являются инструментами режима пользователя, изменить со временем по мере необходимости форматы объектных файлов (сравнительно) просто; надо только «научить» ядро новому формату, и он может быть использован. Часть ядра, загружающая исполняемые файлы, относительно невелика, и это не является невозможной задачей. Поэтому форматы файлов Unix развиваются с течением времени. Первоначальный формат был известен как a.out (Assembler OUTput- вывод сборщика). Следующий формат, до сих пор использующийся в некоторых коммерческих системах, известен как COFF (Common Object File Format- общий формат объектных файлов), а современный, наиболее широко использующийся формат - ELF (Extensible Linking Format - открытый формат компоновки). Современные системы GNU/Linux используют ELF. Ядро распознает, что исполняемый файл содержит двоичный объектный код, проверяя первые несколько байтов файла на предмет совпадения со специальными магическими числами. Это последовательности двух или четырех байтов, которые ядро распознает в качестве специальных. Для обратной совместимости современные Unix-системы распознают несколько форматов. Файлы ELF начинаются с четырех символов «\177ELF». Помимо двоичных исполняемых файлов, ядро поддерживает также исполняемые сценарии (скрипты). Такой файл также начинается с магического числа: в этом случае, это два обычных символа # ! . Сценарий является программой, исполняемой интерпретатором, таким, как командный процессор, awk, Perl, Python или Tel. Строка, начинающаяся с #!, предоставляет полный путь к интерпретатору и один необязательный аргумент: #! /bin/awk -f BEGIN {print, "riello, world"} Предположим, указанное содержимое располагается в файле hello.awk и этот файл исполняемый. Когда вы набираете хhello.awk', ядро запускает программу, как если бы вы напечатали х /bin/awk -f he 11 о. awk7 .Любые дополнительные аргументы командной строки также передаются программе. В этом случае, awk запускает программу и отображает общеизвестное сообщение hello, world. Механизм с использованием #! является элегантным способом скрыть различие между двоичными исполняемыми файлами и сценариями. Если hello.awk переименовать просто в hello, пользователь, набирающий 'hello', не сможет сказать (и, конечно, не должен знать), что hello не является двоичной исполняемой программой. 1.1.4. Устройства Одним из самых замечательных новшеств Unix было объединение файлового ввода- вывода и ввода-вывода от устройств4. Устройства выглядят в файловой системе как файлы, для доступа к ним используются обычные права доступа, а для их открытия, чте- Эта особенность впервые появилась в Multics, но Multics никогда широко не использовался. - Примеч. автора.
1.2. Модель процессов Linux/Unix 25 ния, записи и закрытия используются те же самые системные вызовы ввода-вывода. Вся «магия», заставляющая устройства выглядеть подобно файлам, скрыта в ядре. Это просто другой аспект движущего принципа простоты в действии: мы можем выразить это как никаких частных случаев для кода пользователя. В повседневной практике, в частности, на уровне оболочки, часто появляются два устройства: /dev/null и /dev/tty. /dev/null является «битоприемником». Все данные, посылаемые /dev/null, уничтожаются операционной системой, а все попытки прочесть отсюда немедленно возвращают конец файла (EOF). /dev/tty является текущим управляющим терминалом процесса - тем, который он слушает, когда пользователь набирает символ прерывания (обычно CTRL-C) или выполняет управление заданием (CTRL-Z). Системы GNU/Linux и многие современные системы Unix предоставляют устройства /dev/stdin, /dev/stdout и /dev/stderr, которые дают возможность указать открытые файлы, которые каждый процесс наследует при своем запуске. Другие устройства представляют реальное оборудование, такое, как ленточные и дисковые приводы, приводы CD-ROM и последовательные порты. Имеются также программные устройства, такие, как псевдотерминалы, которые используются для сетевых входов в систему и систем управления окнами, /dev/console представляет системную консоль, особое аппаратное устройство миникомпьютеров. В современных компьютерах /dev/console представлен экраном и клавиатурой, но это может быть также и последовательный порт. К сожалению, соглашения по именованию устройств не стандартизированы, и каждая операционная система использует для лент, дисков и т. п. собственные имена. (К счастью, это не представляет проблемы для того, что мы рассматриваем в данной книге.) Устройства имеют в выводе х Is -1' в качестве первого символа b или с: $ Is -1 /dev/tty /dev/hda brw-rw 1 root disk 3, 0 Aug 31 02:31 /dev/hda crw-rw-rw- 1 root root 5, 0 Feb 26 08:44 /dev/tty Начальная xb' представляет блочные устройства, a sc' представляет символьные устройства. Файлы устройств обсуждаются далее в разделе 5.4, «Получение информации о файлах». 1.2. Модель процессов Linux/Unix Процесс является работающей программой5. Процесс имеет следующие атрибуты: уникальный идентификатор процесса (PID); родительский процесс (с соответствующим идентификатором, PPID); идентификаторы прав доступа (UID, GID, набор групп и т. д.); 5 Процесс может быть приостановлен, в этом случае он не «работающий»; но он и не завершен. В любом случае, на ранних этапах восхождения по кривой обучения не стоит быть слишком педантичным. - Примеч. автора
26 Глава 1. Введение отдельное от всех других процессов адресное пространство; программа, работающая в этом адресном пространстве; • текущий рабочий каталог D.'); • текущий корневой каталог (/; его изменение является продвинутой темой); • набор открытых файлов, каталогов, или и того, и другого; маска запретов доступа, использующаяся при создании новых файлов; набор строк, представляющих окружение6; приоритеты распределения времени процессора (продвинутая тема); • установки для размещения сигналов (signal disposition) (продвинутая тема); управляющий терминал (тоже продвинутая тема). Когда функция main () начинает исполнение, все эти вещи уже помещены в работающей программе на свои места. Для запроса и изменения каждого из этих вышеназванных элементов доступны системные вызовы; их освещение является целью данной книги. Новые процессы всегда создаются существующими процессами. Существующий процесс называется родительским, а новый процесс - порожденным. При загрузке ядро вручную создает первый, изначальный процесс, который запускает программу / sbin/ ini t; идентификатор этого процесса равен 1, он осуществляет несколько административных функций. Все остальные процессы являются потомками init. (Родительским процессом init является ядро, часто обозначаемое в списках как процесс с ГО 0.) Отношение порожденный-родительский является отношением один к одному; у каждого процесса есть только один родитель, поэтому легко выяснить PID родителя. Отношение родительский-порожденный является отношением один ко многим; каждый данный процесс может создать потенциально неограниченное число порожденных. Поэтому для процесса нет простого способа выяснить все PID своих потомков. (Во всяком случае, на практике это не требуется.) Родительский процесс можно настроить так, чтобы он получал уведомление при завершении порожденного процесса, он может также явным образом ожидать наступления такого события. Адресное пространство (память) каждого процесса отделена от адресного пространства всех остальных процессов. Если два процесса не договорились явным образом разделять память, один процесс не может повлиять на адресное пространство другого. Это важно; это обеспечивает базовый уровень безопасности и надежности системы. (В целях эффективности, система разделяет исполняемый код одной программы с правом доступа только для чтения между всеми процессами, запустившими эту программу. Это прозрачно для пользователя и запущенной программы.) Текущий рабочий каталог - это каталог, относительно которого отсчитываются относительные пути файлов (те, которые не начинаются с V). Это каталог, в котором вы находитесь, когда набираете команду оболочки ' cd someplace'. Так называемые переменные окружения. - Примеч. науч. ред.
1.3. Стандартный с против оригинального с 27 По соглашению, все программы запускаются с тремя уже открытыми файлами: стандартным вводом, стандартным выводом и стандартной ошибкой. Это места, откуда принимается ввод, куда направляется вывод и куда направляются сообщения об ошибках соответственно. На протяжении этой книги мы увидим, как они назначаются. Родительский процесс может открыть дополнительные файлы и сделать их доступными для порожденных процессов; порожденный процесс должен каким-то образом-узнать, что они есть, либо посредством какого-либо соглашения, либо через аргументы командной строки или переменную окружения. Окружение представляет собой набор строк, каждая в виде 'имя=значение '. Для запроса и установки значений переменных окружения имеются специальные функции, а порожденные процессы наследуют окружение своих родителей. Типичными переменными окружения оболочки являются PATH и НОМЕ. Многие программы для управления своим поведением полагаются на наличие и значения определенных переменных окружения. Важно понять, что один процесс в течение своего существования может исполнить множество программ. Все устанавливаемые системой атрибуты (текущий каталог, открытые файлы, PID и т. д.) остаются теми же самыми, если только они не изменены явным образом. Отделение «запуска нового процесса» от «выбора программы для запуска» является ключевым нововведением Unix. Это упрощает многие операции. Другие операционные системы, которые объединяют эти две операции, являются менее общими и их сложнее использовать. 1.2.1. Каналы: сцепление процессов Без сомнения, вам приходилось использовать конструкцию (' |') оболочки для соединения двух или более запущенных программ. Канал действует подобно файлу: один процесс записывает в него, используя обычную операцию записи, а другой процесс считывает из него с помощью операции чтения. Процессы (обычно) не знают, что их ввод/вывод является каналом, а не обычным файлом. Как ядро скрывает «магию» для устройств, заставляя их действовать подобно файлам, точно так же оно проделывает эту работу для каналов, принимая меры по задержке записи в канал при его наполнении и задержке чтения, когда нет ожидающих чтения данных. Таким образом, принцип файлового ввода/выврда применительно к каналам служит ключевым механизмом для связывания запущенных программ; не требуется никаких временных файлов. Опять-таки общность и простота работы: никаких особых случаев для кода пользователя. 1.3. Стандартный С против оригинального С В течение многих лет определение С де-факто можно было найти в первом издании книги Брайана Кернигана и Денниса Ричи «Язык программирования С» (Brian Kernighan & Dennis Ritchie, The С Programming Language). Эта книга описала С, как он существовал для Unix и на системах, на которые его перенесли разработчики лаборатории Bell Labs. На протяжении данной книги мы называем его как «оригинальный С», хотя обычным явля-
2& Глава 1 < Введение ется также название «С Кернигана и Ричи» («K&R С»), по именам двух авторов книги. (Деннис Ричи разработал и реализовал С.) Стандарт ISO С 1990 г.7 формализовал определения языка, включая функции библиотеки С (такие, как printf () и f open ()). Комитет по стандартам С проделал замечательную работу по стандартизации существующей практики и избежал введения новых возможностей, с одним значительным исключением (и несколькими незначительными). Наиболее заметным изменением языка было использование прототипов функций, заимствованных от C++. Стандартные языки программирования С, C++ и Java используют прототипы функций для объявлений и определений функций. Прототип описывает не только возвращаемое значение функции, но также и число и тип ее аргументов. С прототипами компилятор может выполнить проверку типов в точке вызова функции: Объявление extern int myfunc(struct my__struct *a, struct my_struct *b, double c, int d); Определение int myfunc (struct my__struct *a, struct my_struet *b, double c, int d) { } struct my_struct s, t; int j ; /* Вызов функции, где-то в другом месте: */ j = my_func(& s, & t, 3.1415, 42); Это правильный вызов функции. Но рассмотрите ошибочный вызов: j = my_func(-l, -2,.0); /* Ошибочные число и типы аргументов */ Компилятор может сразу же определить этот вызов как неверный. Однако, в оригинальном С функции объявляются без указания списка аргументов: extern int myfunc(); /* Возвращает int, аргументы неизвестны */ Более того, определения функций перечисляют имена параметров в заголовке функции, затем объявляют параметры перед телом функции. Параметры типа int о&ьявлять не нужно, и если функция возвращает int, его тоже не нужно объявлять: myfunc (a, b, с, d) /* Возвращаемый тип int*/ struct my_struct *a, *b; double с; /.* Обратите внимание, нет объявления параметра d*/ 7 Международный стандарт ISO/IEC 9899-1990 описывает разновидность языка С известную также как С89 или С90. - Примеч. науч. ред.
1.3. Стандартный с против оригинального с 29 { ¦ ' } Рассмотрите снова тот же ошибочный вызов функции: * j = my_f unc (-1, -2 , 0) ; '. В оригинальном С у компилятора нет возможности узнать, что вы (ошибочно, полагаем) передали myi_f unc () ошибочные аргументы. Подобные ошибочные вызовы обычно приводят к трудно устранимым проблемам времени исполнения (таким, как ошибки сегментации, из-за чего программа завершается), и для работы с такими вещами была создана программа Unix lint. Поэтому, хотя прототипы функции и были радикальным отходом от существующей практики, дополнительную проверку типов посчитали слишком важной, чтобы обходиться без нее, и после небольшого сопротивления она была добавлена в язык. Для С стандарта 1990 г. код, написанный в оригинальном стиле, является действительным как для объявлений, так и для определений. Это дает возможность продолжать компилировать миллионы строк существующего кода с помощью компилятора, удовлетворяющего стандарту. Новый код, очевидно, должен быть написан с прототипами из-за улучшенных возможностей проверки ошибок времени компилирования. Стандарт С 1999^ г.8 продолжает допускать объявления и определения в оригинальном стиле. Однако, правило «неявного int» было убрано; функции должны иметь возвращаемый тип, а все параметры должны быть объявлены. Более того, когда программа вызывала функцию, которая не была формально объявлена, оригинальный С создал бы для функции неявное объявление с возвращаемым типом int. С стандарта 1999 г. делал то же самое, дополнительно отметив, что у него не было информации о параметрах. С стандарта 1999 г. не предоставляет больше возможности «автоматического объявления». Другими заметными дополнениями в стандарте С являются ключевое слово const, также из C++, и ключевое слово volatile, которое придумал комитет. Для кода, который вы увидите в этой книге, наиболее важной вещью является понимание различных синтаксисов объявлений и определений функций. Для кода V7, использующего определения в оригинальном стиле, мы добавили комментарии, показывающие эквивалентный прототип. В остальных случаях мы оставили код как есть, предпочитая показать его точно таким, каким он был первоначально написан, и как бы вы его увидели, если бы сами загрузили код. Хотя стандарт С 1999 г. добавляет некоторые дополнительные ключевые слова и возможности, отсутствующие в версии 1990 г., мы решили придерживаться диалекта 1990 г., поскольку компиляторы С99 не являются пока типичными. Практически, это не имеет значения: код С89 должен компилироваться и запускаться без изменений при использовании компилятора С99, а новые возможности С99 не затрагивают наше обсуждение или использование фундаментальных API Linux/Unix. Стандарт С99 (ISO/IEC 9899-1999). - Примеч. науч. ред.
30 Глава 1. Введение 1.4. Почему программы GNU лучше Что делает программу GNU программой GNU9? Что делает программное обеспечение GNU «лучше» по сравнению с другим (платным или бесплатным) программным обеспечением? Наиболее очевидной разницей является общедоступная лицензия (General Public Liense - GPL), которая описывает условия распространения для программного обеспечения GNU. Но это обычно не причина, чтобы вы могли услышать, как люди говорят: «Дайте GNU-версию xyz, она намного лучше». Программное обеспечение GNU в общем более устойчиво, имеет лучшую производительность, чем в стандартных версиях Unix. В данном разделе мы рассмотрим некоторые причины этого явления, а также рассмотрим документ, описывающий принципы проектирования программного обеспечения GNU. «Стандарты кодирования GNU» (GNU Coding Standards) описывают создание программного обеспечения для проекта GNU. Они охватывает ряд тем. Вы можете найти GNU Coding Standards по адресу http: / /www. gnu. org/prep/standards . html. Смотрите в онлайн-версии ссылки на исходные файлы в других форматах.. В данном разделе мы описываем лишь те части GNU Coding Standards, которые относятся к проектированию и реализации программ. 1.4.1. Проектирование программ Глава 3 GNU Coding Standards содержит общие советы относительно проектирования программ. Четырьмя главными проблемами являются совместимость (со стандартами и с Unix), язык, использование нестандартных возможностей других программ (одним словом, «ничего»), и смысл «переносимости». Важной целью является совместимость со стандартом С и POSIX, а также, в меньшей степени, с Berkley Unix. Но она не преобладает. Общей идеей является предоставление всех необходимых возможностей через аргументы командной строки для предоставления точного режима ISO или POSIX. Предпочтительным языком для написания программного обеспечения GNU является С, поскольку это наиболее доступный язык. В мире Unix стандарт С теперь обычен, но если для вас не представляет труда поддержка оригинального С, вы должны сделать это. Хотя стандарты кодирования отдают предпочтение С перед C++, C++ теперь тоже вполне обычен. Примером широко используемого пакета GNU, написанного на C++, является grof f (GNU trof f). Наш опыт говорит, что с GCC, поддерживающим C++, установка grof f не представляет сложности. Этот раздел адаптирован из статьи автора, который издавался в выпуске 16 Linux Journal. (См. http: // www. linuxjournal. com/article .php?sid=1135). Перепечатано и адаптировано по разрешению. - Примеч. автора.
1.4. Почему программы GNU лучше 31 Стандарты утверждают, что переносимость является чем-то вроде отвлекающего маневра. Утилиты GNU ясно нацелены на работу с ядром GNU и с библиотекой GNU С10. Но поскольку ядро еще не завершено, и пользователи используют инструменты GNU на не-GNU системах, переносимость желательна, но не является первостепенной задачей. Стандарт рекомендует для достижения переносимости между различными системами Unix использовать Autoconf. / ¦ 1.4.2. Поведение программы Глава 4 GNU Coding Standards предоставляет общие советы относительно поведения программы. Ниже мы вернемся к одному из ее разделов для более подробного рассмотрения. Глава фокусируется на строении программы, форматировании сообщений об ошибках, написании библиотек (делая их рентабельными) и стандартах для' интерфейса командной строки. Форматирование сообщений об ошибках важно, поскольку несколько инструментов, особенно Emacs, используют сообщения, об ошибках, чтобы помочь вам попасть в то место в исходном файле или файле данных, где произошла ошибка. Утилиты GNU должны использовать для обработки командной строки функцию getopt_long (). Эта функция предусматривает разбор аргументов командной строки как для опций в стиле традиционного Unix (х gawk -F: . . . '), так и для длинных опций в стиле GNU Dgawk --f ield-separator= :...'). Все программы должны предусматривать опции --help и --version, а когда в одной программе используется длинное имя, оно таким же образом должно использоваться и в другой программе GNU. Для этой цели есть довольно полный списЪк длинных опций, используемых современными GNU-программами. В качестве простого, но очевидного примера, --verbose пишется точно таким же способом во всех GNU-программах. Сравните это с -v, -V, -d и т. д. во многих других программах Unix. Большая часть главы 2, «Аргументы, опции и окружение», с. 23, посвящена механике разбора аргументов и опций. 1.4.3. Программирование на С Наиболее привлекательной частью GNU Coding Standards является глава 5, которая описывает написание кода на С, освещая такие темы, как форматирование кода, правильное использование комментариев, чистое использование С, именование ваших функций и переменных, а также объявление или не объявление стандартных системных функций, которые вы хотите использовать. Форматирование кода является религиозной проблемой; у многих людей разные стили, которые они предпочитают. Лично нам не нравится стиль FSF, и если вы взглянете на gawk, 10 Это утверждение относится к ядру HURD, которое все еще находится в стадии разработки (в начале 2004 г). Разработка на основе GCC и библиотеки GNU С (GLIBC) сегодня имеет место большей частью на Linux- системах. - Примеч. автора.
32 Глава 1. Введение который мы поддерживаем, вы увидите, что он форматирован в стандартном стиле K&R (стиль расположения кода, использованный в обоих изданиях книги Кернигана и Ричи). Но это единственное отклонение в gawk от этой части стандартов кодирования. Тем не менее, хотя нам и не нравится стиль FSF11, мы чувствуем, что при модификации некоторых других программ, придерживание уже использованного стиля кода является исключительно важным. Последовательность в стиле кода более важна, чем сам стиль, который вы выбираете. GNU Coding Standards дает такой же совет. (Иногда невозможно обнаружить последовательный стиль кода, в этом случае программа, возможно, испорчена использованием indent от GNU или cb от Unix.) Что мы сочли важным в главе о написании кода на С, это то, что эти советы хороши для любого кода на С, а не только когда вы работаете над программрй GNU. Поэтому, если вы просто учите С или даже если вы уже работали некоторое время на С (или C++), мы рекомендуем вам эту главу, поскольку она заключает в себе многолетний опыт. 1.4.4. Вещи, которые делают программы GNU лучше Теперь мы рассмотрим раздел, озаглавленный «Написание надежных программ», в главе 4 «Поведение программ для всех программ». Этот раздел описывает принципы проектирования программного обеспечения, которые делают программы GNU лучше их двойников в Unix. Мы процитируем выбранные части главы, с несколькими примерами случаев, в которых эти принципы окупились. Избегайте произвольных ограничений длины или числа любой структуры данных, включая имена файлов, строки, файлы и символы, выделяя все структуры данных динамически. В большинстве инструментов Unix «длинные строки молча срезаются». Это неприемлемо в инструменте GNU. Это правило, возможно, единственное наиболее важное в проектировании программного обеспечения GNU - никаких произвольных ограничений. Все инструменты GNU должны быть способны обрабатывать произвольные объемы данных. Хотя это требование, возможно, усложняет работу программиста, оно облегчает жизнь пользователю. С одной стороны, у нас есть пользователь gawk, регулярно запускающий программу awk для более чем 650 000 файлов (нет, это не опечатка) для сбора статистики, gawk заняла бы более 192 мегабайтов пространства данных, и программа работала бы в течение 7 часов. Он не смог бы запустить эту программу, используя другую реализацию awk12. Утилиты, читающие файлы, не должны удалять символы NUL или любые другие неото- бражаемые символы, включая символы с кодами больше 0177. Единственными здравыми Стиль расположения кода, рекомендуемый фондом свободного программного обеспечения (Free Software Foundation). - Примеч. науч. ред. Эта ситуация имела место примерно в 1993 г.; трюизм даже более очевиден сегодня, когда пользователи обрабатывают с помощью gawk гигабайты протокольных файлов. - Примеч. автора.
1.4. Почему программы GNU лучше 33 исключениями были бы утилиты, специально предназначенные для связывания с определенными типами терминалов или принтеров, которые не могут обработать эти символы. * Также хорошо известно, что Emacs может редактировать любые произвольные файлы, включая файлы, содержащие двоичные данные! По возможности, программы должны обрабатывать должным образом последовательности байтов, представляющих многобайтные символы, используя такие кодировки, как UTF-8 и другие . Каждый системный вызов проверяйте на предмет возвращенной ошибки, если вы не хотите игнорировать ошибки. Включите текст системной ошибки (от perror или эквивалентной функции) в каждое сообщение об ошибке, возникшей при неудачном системном вызове, также, как и имя файла, если он есть, и имя утилиты. Простого «невозможно открыть foo.с» или «ошибка запуска» недостаточно. Проверка каждого системного вызова создает устойчивость. Это еще один случай, когда жизнь программиста труднее, а пользователя легче. Подробно описанное сообщение об ошибке значительно упрощает нахождение и разрешение проблем14. Наконец, мы цитируем главу 1 GNU Coding Standards, которая обсуждает, как написать вашу программу способом, отличным от того, каким написаны программы Unix. Например, утилиты Unix обычно оптимизированы для минимизации использования памяти; если вы взамен хотите получить скорость, ваша программа будет сильно отличаться. Вы можете хранить весь входной фат в ядре и сканировать его там, вместо использования stdio. Используйте недавно открытый более изягцный алгоритм вместо алгоритма Unix программы. Исключите использование временных фатов. Делайте это в один проход вместо двух (мы сделали это на ассемблере). Или, напротив, сделайте упор на простоте вместо скорости. Для некоторых приложений скорость сегодняшних компьютеров делает адекватными более простые алгоритмы. Или выберите обобщение. Например, программы Unix часто содерэюат статичные таблицы или строки фиксированного размера, которые создают произвольные ограничения; используйте вместо этого динамическое выделение памяти. Убедитесь, что ваша программа обрабатывает во входных фатах символы NUL и другие курьезные символы. Добавьте язык программирования для расширяемости и напишите часть программы на этом языке. Или выделите части программы в независимо используемые библиотеки. Или используйте простой сборщик мусора вместо точного отслеживания, когда освобождать память, или используйте новую возможность GNU, такую как obstacks. Великолепным примером того, какое отличие можно сделать в алгоритме, является GNU dif f. Одним из первых ранних воплощений нашей системы было AT&T 3B1: система с процессором МС68010, огромными двумя мегабайтами памяти и 80 мегабайтами на диске. Мы проделали (и делаем) кучу исправлений в руководстве для gawk, файле длиной почти 28 000 строк (хотя в то время он был лишь в диапазоне 10 000 строк). Раздел 13.4, «Не могли бы вы произнести это для меня по буквам?», с. 521, дает обзор многобайтных символов и кодировок. - Примеч. автора. 4 Механика проверки ошибок и сообщений о них обсуждаются в разделе 4.3, «Обнаружение неправильной работы» - Примеч. автора. 2-159
34 Глава 1. Введение Обычно мы частенько использовали xdif f . -с ', чтобы посмотреть на сделанные нами изменения. На этой медленной системе переключение на GNU dif f показало ошеломительную разницу во времени появления контекста dif f. Разница почти всецело благодаря лучшему алгоритму, который использует GNU dif f. В последнем параграфе упоминается идея структурирования программы как независимо используемой библиотеки, с оболочкой командной строки или другим окружающим ее интерфейсом. Примером этого является GDB, отладчик GNU, который реализован в виде инструмента с интерфейсом командной строки поверх отладочной библиотеки. (Разделение основных возможностей GDB от интерфейса командной строки является продолжающимся проектом). Эта реализация дает возможность создать поверх отладочных функциональных возможностей графический интерфейс отладчика. 1.4.5. Заключительные соображения по поводу «GNU Coding Standards» GNU Coding Standards является стоящим для прочтения документом, если вы хотите разрабатывать новое программное обеспечение GNU, обмениваться существующими программами GNU или просто научиться программировать лучше. Принципы и методики, которые она поддерживает - вот что делает программное обеспечение GNU предпочитаемым выбором в сообществе Unix. 1.5. Пересмотренная переносимость Переносимость является чем-то вроде Святого Грааля; всегда недостающим впоследствии, но не всегда достижимым и определенно нелегким. Есть несколько аспектов написания переносимого кода. GNU Coding Standards обсуждает многие из них. Но есть и другие стороны. При разработке принимайте переносимость во внимание как на высоком, так и на низком уровнях. Мы рекомендуем следующие правила: Соответствуйте стандартам Хотя это может потребовать напряжения, знакомство с формальными стандартами языка, который вы используете, окупается. В частности, обратите внимание на стандарты ISO 1990 и 1999 гг. для С и стандарт 2003 г. для C++, поскольку большинство программ Linux создано на одном из этих двух языков. В промышленности также широко поддерживается стандарт POSIX для интерфейса библиотечных и системных вызовов, хотя он и большой. Написание в соответствии с POSIX значительно повышает шансы успешного переноса вашего кода и на другие системы, помимо GNU/Linux. Этот стандарт вполне читабелен; он концентрирует в себе десятилетия опыта и хорошей практики. Выбирайте для работы лучший интерфейс Если стандартный интерфейс выполняет нужную вам работу, используйте его в своем коде. Для обнаружения недоступного интерфейса используйте Autoconf, и добавьте его
1.6. Рекомендуемая литература 35 замещающую версию для ограниченной системы. (Например, на некоторых более старых системах отсутствует функция memmove (), которую довольно легко запрограммировать самому или вставить из библиотеки GLIBC.) Изолируйте проблемы переносимости за новыми интерфейсами Иногда вам может потребоваться выполнить специфичные для операционной системы задачи, которые можно исполнить на одних системах, но нельзя на других. (Например, на некоторых системах каждая программа должна сама раскрывать групповые символы в командной строке, вместо выполнения этой работы командным процессором.) Создайте новый интерфейс, который ничего не делает в системах, которым он не нужен, но проделывает необходимую коррекцию для систем, которые в этом нуждаются. Используйте для конфигурирования Autoconf По возможности избегайте #if def. Если это невозможно, скройте его в низкоуровневом библиотечном коде. Для проверки тестов, которые должны исполняться с помощью #if def, используйте Autoconf. 1.6. Рекомендуемая литература 1. The С Programming Language, 2nd edition, by Brian W. Kernighan and Dennis M. Ritchie. Prentice-Hall, Englewood Cliffs, New Jersey, USA, 1989. ISBN: 0-13-110370-915. Это «библия» С, охватывающая версию стандарта С 1990 г. Это довольно сжатая книга, с большим количеством информации, помещенной в поразительно небольшое число страниц. Вам может потребоваться прочитать ее более одного раза; это стоит затраченных усилий. 2. С, A Reference Manual, 5th edition, by Samuel P. Harbison III and Guy L. Steele, Jr. Prentice-Hall, Upper Saddle River, New Jersey, USA, 2002. ISBN: 0-13-089592-X. Это тоже классическая книга. Она охватывает оригинальный С, а также стандарты 1990 и 1999 гг. Поскольку она современна, она служит ценным дополнением к первой книге. Она охватывает многие важные темы, такие, как интернациональные типы и библиотечные функции, которых нет в книге Кернигана и Ричи. 3. Notes on Programming in С, by Rob Pike, February 21,1989. Доступна через множество веб-сайтов. Возможно, чаще всего упоминаемым местом является http:// www. lysator. liu. se/c/pikestyle.html. (Многие другие полезные статьи доступны там же на один уровень выше: http: //www. lysator. liu.se/c/ .) Роб Пайк много лет работал в исследовательском центре Bell Labs, где были созданы С и Unix, и проводил там изыскания. Его замечания концентрируют многолетний опыт в «философию ясности в программировании», это стоит прочтения. 5 Русский перевод: Брайн Керниган, Денис Ритчи. Язык програмирования Си (изд. 3-е исправленное). Санкт- Петербург, Невский диалект, 2001 - Примеч. науч. ред.
36 Глава 1. Введение 4. Различные ссылки на .http://www.chris-lott.pr-g/resources/cstyle/. Этот сайт включает заметки Роба Пайка и несколько статей Генри Спенсера (Henry Spencer). Особенно высокое положение занимает «Рекомендуемый стиль С и стандарты программирования» (Recommended С Style and Coding Standards), первоначально написанный на сайте Bell Labs Indian Hill. 1.7. Резюме «Файлы и процессы» суммируют мировоззрение Linux/Unix. Трактовка файлов как потоков байтов, а устройств как файлов, и использование стандартных ввода, вывода и ошибки упрощают построение программ и унифицируют модель доступа к данным. Модель прав доступа проста, но гибка, и приложима как к файлам, так и каталогам. Процессы являются работающими программами, у которых есть связанные с ними идентификаторы пользователя и группы для проверки прав доступа, а также другие атрибуты, такие, как открытые файлы и текущий рабочий каталог. • Наиболее заметным различием между стандартным С и оригинальным С является использование прототипов функций для более строгой проверки типов. Хороший программист на С должен быть способен прочесть код, написанный в оригинальном стиле, поскольку его используют многие существующие программы. Новый код должен быть написан с использованием прототипов. GNU Coding Standards описывает написание программ GNU. Она предусматривает многочисленные ценные методики и руководящие принципы для создания надежного, практичного программного обеспечения. Принцип «никаких произвольных ограничений» является, возможно, единственным наиболее важным из них. Этот документ является обязательным для прочтения серьезными программистами. Переносимость программ является сложной проблемой. Руководящие указания и инструментарий помогают, но в конечном счете нужен также и опыт. Упражнения 1. Прочтите и прокомментируйте статью Ричарда М. Столмена «Проект GNU» (Richard M. Stallman, «The GNU Project»I6, первоначально написанную в августе 1998 г. http: / /www. gnu. org/gnu/ thegnupro j ect. html - Примеч. автора
Глава 2 Аргументы, опции и переменные окружения В этой главе: 2.1. Соглашения по опциям и аргументам 37 2.2. Базовая обработки командной строки 41 2.3. Разбор опций: getopt () и getopt_long () . , 43 2.4. Переменные окружения 52 2.5. Резюме 61 Упражнения 61 Первой задачей любой программы обычно является интерпретация опций и аргументов командной строки. Данная глава рассматривает, как программы С (и C++) получают аргументы своей командной строки, описывает стандартные процедуры для разбора опций и бросает взгляд на переменные окружения. 2.1. Соглашения по опциям и аргументам У слова аргументы есть два значения. Более техническим определением является «все 'слова' в командной строке». Например: $ Is main.с opts.с process.с Здесь пользователь напечатал четыре «слова». Все четыре слова сделаны доступными программе в качестве ее аргументов1. Второе определение более неформальное: аргументами являются все слова командной строки, за исключением имени команды. По умолчанию, оболочки Unix отделяют аргументы друг от друга разделителями (пробелами или символами TAB). Кавычки позволяют включать в аргументы разделитель: $ echo here are lots of spaces here are lots of spaces /* Оболочка «съедает» пробелы */ $ echo "here are lots of spaces" here are lots of spaces /* Пробелы остались */ Имя команды - Is в данном примере, так же доступно программе в качестве аргумента. - Примеч. науч. ред.
38 Глава 2. Аргументы, опции и переменные окружения Кавычки прозрачны для запущенной программы; echo никогда не видит символов двойной кавычки. (В оболочке двойные и одинарные кавычки различаются; обсуждение этих правил выходит за рамки данной книги, которая фокусируется на программировании на С.) Аргументы можно подразделить далее на опции и операнды. В предыдущих двух примерах все аргументы были операндами: файлы для Is и простой текст для echo. Опции являются специальными аргументами, которые каждая программа интерпретирует. Опции изменяют поведение программы или предоставляют программе информацию. По старому соглашению, которого (почти) всегда придерживаются, опции.начинаются сверточки (т. е. дефиса, значка минус), и состоят из единственной буквы. Аргументы опции являются информацией, необходимой для опции, в отличие от обычных аргументов-операндов. Например, опция -f программы fgrep означает «использовать содержимое следующего файла в качестве списка строк для поиска». См. рис. 2.1. Имя команды Опция Аргумент опции 7 Операнды t t т t • т т ер -? patfile foo.c bar.с baz Рис. 2.1. Компоненты командной строки Таким образом, patfile является не файлом данных для поиска, а предназначен для использования fgrep в определении списка строк, которые нужно искать. 2.1.1. Соглашения POSIX Стандарт POSIX описывает ряд соглашений, которых придерживаются удовлетворяющие стандарту программы. Никто от вас не требует, чтобы ваши программы удовлетворяли этим стандартам, но это хорошая мысль сделать так: пользователи Linux и Unix по всему миру понимают и используют эти соглашения, и если вы не будете им следовать, ваши пользователи будут несчастны. (Или у вас вообще не будет пользователей!) Более того, функции, которые мы обсуждаем далее в этой главе, освобождают вас от бремени ручной реализации этих соглашений для каждой программы, которую вы пишете. Вот эти правила, перефразированные из стандарта: 1. В имени программы должно быть не менее двух и не более девяти символов. 2. Имена программ должны содержать лишь строчные символы и цифры.
2.1. Соглашения по опциям и аргументам 39 3. Имя опции должно быть простым буквенно-цифровым символом. Опции с множеством цифр не должны допускаться. Для производителей, реализующих утилиты POSIX, опция -W зарезервирована для специфичных для производителя опций. 4. Все опции должны начинаться с символа '-'. 5. Для опций, не требующих аргументов, должно быть возможно объединение нескольких опций после единственного символа '-'. (Например, 'f oo -a -b -с' и 'f oo -abc' должны интерпретироваться одинаково.) 6. Когда опции все же требуется аргумент, он должен быть отделен от опции пробелом (например, 4fgrep -f patfile'). Однако, стандарт допускает историческую практику, при которой иногда опция и ее операнд могут находиться в одной строке: 4fgrep -fpatf ile\ На практике функции getopt () ngetopt__long() интерпретируют'-fpatf ile' как'-f patfile',ане как 4-f -p -a -t . . .'. ' 7. Аргументы опций не должны быть необязательными. Это означает, что если в документации программы указано, что опции требуется аргумент, этот аргумент должен присутствовать всегда, иначе программа потерпит неудачу. GNU getopt () все же предусматривает необязательные аргументы опций, поскольку иногда они полезны. 8. Если опция принимает аргумент, который может иметь несколько значений, программа должна получать этот аргумент в виде одной строки со значениями, разделенными запятыми или разделителем. Например, предположим, что гипотетической программе myprog требуется список пользователей для опции -и. Далее она может быть вызвана одним из двух способов: myprog -u "arnold,joe,jane" /* Разделение запятыми */' myprog -u "arnold joe jane" /* Разделение пробелами */ В таком случае вы должны самостоятельно отделить и обработать каждое значение (т. е. здесь нет стандартной процедуры), но ручная реализация обычно проста. 9. Опции должны находиться в командной строке первыми, перед операндами. Версии getopt () Unix проводят в жизнь это соглашение. GNU getopt () по умолчанию этого не делает, хотя вы можете настроить его на это. 10. Специальный аргумент '--' указывает на окончание всех опций. Все последующие аргументы командной строки рассматриваются как операнды, даже если они начинаются с черточки. 11. Порядок, в котором приведены опции, не должен играть роли. Однако, для взаимно исключающих опций, когда одна опция перекрывает установки другой, тогда (так сказать) последняя побеждает. Если опция, имеющая аргумент, повторяется, программа должна обработать аргументы по порядку. Например, 'myprog -u arnold -и jane' то же самое, что и 'myprog -u "arnold, jane"'. (Вам придется осуществить это самостоятельно; getopt () вам не поможет.) 12. Нормально, когда порядок аргументов имеет для программы значение. Каждая программа должна документировать такие вещи.
40 Глава 2. Аргументы, опции и переменные окружения 13. Программы, читающие или записывающие именованные файлы, должны трактовать единственный аргумент '-' как означающий стандартный ввод или стандартный вывод, в зависимости от того, что подходит программе. Отметим, что многие стандартные программы не следуют всем указанным соглашениям. Главной причиной является историческая совместимость; многие такие программы предшествовали систематизации этих соглашений. 2.1.2. Длинные опции GNU Как мы видели в разделе 1.4.2 «Поведение программ», программам GNU рекомендуется использовать длинные опции в форме --help, --verbose и т. д. Такие опции, поскольку они начинаются с '--', не конфликтуют с соглашениями POSIX. Их также легче запомнить, и они предоставляют возможность последовательности среди всех утилит GNU. (Например, --help является везде одним и тем же, в отличие от -h для «help», -i для «information» и т. д.) Длинные опции GNU имеют свои собственные соглашения, реализованные в функции getopt_long () : 1. У программ, реализующих инструменты POSIX, каждая короткая опция (один символ) должна иметь также свой вариант в виде длинной опции. 2. Дополнительные специфические для GNU опции не нуждаются в соответствующей короткой опции, но мы рекомендуем это сделать. 3. Длинную опцию можно сократить до кратчайшей строки, которая остается уникальной. Например, если есть две опции --verbose и --verbatim, самыми короткими сокращениями будут --verbo и --verba. 4. Аргументы опции Отделяются от длинных опций либо разделителем, либо символом =. Например, --sourcefile=/some/file или --sourcefile /some/file. 5. Опции и аргументы могут быть заинтересованы в операндах командной строки; getopt_long() переставляет аргументы таким образом, что сначала обрабатываются все опции, а затем все операнды доступны последовательно. (Такое поведение можно запретить.) 6. Аргументы опций могут быть необязательными. Для таких опций считается, что аргумент присутствует, если он находится в одной строке с опцией. Это работает лишь для коротких опций. Например, если -х такая опция и дана строка ' f oo -xYANKEES -у', аргументом -х является 'YANKEES'. Для 'f сю -х -у' у -х нет аргументов. 7. Программы могут разрешить длинным опциям начинаться с одной черточки. (Это типично для многих программ X Window.) Многое из этого станет яснее, когда позже в этой главе мы рассмотрим getopt_long(). GNU Coding Standards уделяет значительное место перечислению всех длинных и коротких опций, используемых программами GNU. Если вы пишете программу, использующую длинные опции, посмотрите, нет ли уже использующихся имен опций, которые имело бы смысл использовать и вам.
2.2. Базовая обработка командной строки 41 2.2. Базовая обработка командной строки Программа на С получает доступ к своим аргументам командной строки через параметры argc и argv. Параметр argc является целым, указывающим число имеющихся аргументов, включая имя команды. Есть два обычных способа определения main(), отличающихся способом объявления argc: int inain(int argc, char *argv[]) int main(int argc, char **argv) { { } } Практически между двумя этими объявлениями нет разницы, хотя первое концептуально более понятно: argc является массивом указателей на символы. А второе определение технически более корректно, это то, что мы используем. На рис. 2.2 изображена эта ситуация. char ** char * "> "cat" Строки С, завершающиеся' \0' ¦> "filel" ¦> "file2" Указатель NULL, двоичный ноль Рис- 2.2. Память для argc По соглашению, argv [ 0 ] является именем программы. (Детали см. в разделе 9.1.4.3. «Имена программ и argv [ 0 ]».) Последующие элементы являются аргументами командной строки. Последним элементом массива argv является указатель NULL. argc указывает, сколько имеется аргументов; поскольку в С индексы отсчитываются с нуля, выражение 'argv[argc] == NULL' всегда верно. Из-за этого, особенно в коде для Unix, вы увидите различные способы проверки окончания списка аргументов, такие, как цикл с проверкой, что счетчик превысил argc, или 'argv[i] == 0', RiiH'*argv != NULL' и т. д. Они все эквивалентны. 2.2.1. Программа echo V7 Возможно, простейшим примером обработки командной строки является программа V7 echo, печатающая свои аргументы в стандартный вывод, разделяя их пробелами и завершая символом конца строки. Если первым аргументом является -п, завершающий символ новой строки опускается. (Это используется для приглашений из сценариев оболочки.) Вот код2: См. /usr/src/cmd/echo. с в дистрибутиве VI.-Примеч. автора. argv
42 Глава 2. Аргументы, опции и переменные окружения I #include <stdio.h> 2 3 main(argc, argv) /*int main(int argc, char **argv)*/ 4 int argc; 5 char *argv[]; 6 { 7 register int i, nflg; 8 9 nflg = 0; 10 if(argc > 1 && argv[l][0] == •-¦ && argv[l][1] == 'n') { II nflg++; 12 argc—; 13 argv++; 14 } 15 for(i=l; i<argc; i++) { 16 fputs(argv[i], stdout); 17 if (i < argc-1) 18 putchar(* ')/ 19 } 20 if(nflg == 0) 21 putchar('\n'); 22 exit@); 23 } Всего 23 строки! Здесь есть два интересных момента. Во-первых, уменьшение argc и одновременное увеличение argv (строки 12 и 13) являются обычным способом пропуска начальных аргументов. Во-вторых, проверка наличия -п (строка 10) является упрощением, -no-newline-at-the-end также работает. (Откомпилируйте и проверьте это!) Ручной разбор опций обычен для кода V7, поскольку функция get opt () не была еще придумана. Наконец, здесь и в других местах по всей книге, мы видим использование ключевого слова register. Одно время это ключевое слово давало компилятору подсказку, что данная переменная должна по возможности размещаться в регистре процессора. Теперь это ключевое слово устарело; современные компиляторы все основывают размещение переменных в регистрах на анализе исходного кода, игнорируя ключевое слово register. Мы решили оставить использующий это слово код, как есть, но вы должны знать, что оно больше не имеет реального применения3. Когда мы спросили Джима Мейеринга (Jim Meyering), сопроводителя Coreutils, о наличии register в GNU Coreutils, он дал нам интересный ответ. Он удаляет эти слова при изменении кода, но в остальных случаях оставляет их на месте, чтобы облегчить интеграции сделанных изменений с существующими версиями. - Примеч. автора.
2.3. Разбор опций: getopt () и getopt_long () 43 2.3. Разбор опций: getopt () и getopt_long () Примерно в 1980-х группа поддержки Unix для System III в AT&T заметила, что каждая программа Unix использовала Для разбора аргументов свои собственные методики. Чтобы облегчить работу пользователей и программистов, они разработали большинство из перечисленных ранее соглашений. (Хотя изложение в System III справки для intro(\) значительно менее формально, чем в стандарте POSIX.) Группа поддержки Unix разработала также функцию getopt (), вместе с несколькими внешними переменными, чтобы упростить написание кода, придерживающегося стандартных соглашений. Функция GNU getopt_long () предоставляет совместимую с getopt () версию, а также упрощает разбор длинных опций в описанной ранее форме. 2.3.1. Опции с одним символом Функция getopt () объявлена следующим образом: #include <unistd.h> /*POSIX*/ int getopt(int argc, char *const argv[], const char *optstring); extern char *optarg; extern int optind, opterr, optopt; Аргументы argc и argv обычно передаются непосредственно от main () . optstring является строкой символов опций. Если за какой-либо буквой в строке следует двоеточие, эта опция ожидает наличия аргумента. Для использования getopt (•) вызывайте ее повторно из цикла while до тех пор, пока она не вернет -1. Каждый раз, обнаружив действительный символ опции, функция возвращает этот символ. Если опция принимает аргумент, указатель на него помещается в переменную optarg. Рассмотрим программу, принимающую опцию -а без аргумента и опцию -Ь с аргументом: int ос; /* символ опции */ char *fcL_opt_arg; while ((ос "= getopt(argc, argv, "ab:")) != -1) { switch (oc) { case 'a * : /* обработка -а, установить соответствующий флаг */ break; .case 'b': /* обработка -b, получить значение аргумента из optarg */ b_opt_arg = optarg; break; case ' : ' : ... /* обработка ошибок, см. текст'*/ case '?': default:
44 Глава 2. Аргументы, опции и переменные окружения ... /* обработка ошибок, см. текст */ } }¦¦•/• В ходе работы getopt () устанавливает несколько переменных, контролирующих обработку ошибок. char *optarg Аргумент для опции, если она принимает аргумент. int optind Текущий индекс в argv. Когда цикл loop завершается, оставшиеся операнды находятся с argv{optind] по argv[argc-l]. (Помните, что 'argvfargc] ==NULL\) int opterr Когда эта переменная не равна нулю (значение по умолчанию), getopt () печатает свои собственные сообщения для недействительных опций или отсутствующих аргументов опций. int optopt Когда находится недействительный символ опции, getopt () возвращает либо '?', либо fc:' (см. ниже), a optopt содержит обнаруженный недействительный символ. ЛюДи есть люди, программы неизбежно будут иногда вызываться неправильно либо с недействительной опцией, либо с отсутствующим аргументом опции. Обычно в таких случаях getopt () выводит свои собственные сообщения и возвращает символ '?'. Однако, вы можете изменить такое поведение двумя способами. Во-первых, записав 0 в opterr перед вызовом getopt (), можно заставить getopt () не предпринимать при обнаружении проблем никаких действий. Во-вторых, если первый символ в optstring является двоеточием, getopt () не предпринимает никаких действий и возвращает другой символ в зависимости от ошибки следующим образом: Неверная опция getopt () возвращает '?', a optopt содержит неверный символ опции. (Это обычное поведение.) Отсутствует аргумент опции getopt () возвращает ':'. Если первый символ optstring не является двоеточием, getopt () возвращает '?', делая этот случай неотличимым от случая неверной опции. Таким образом, помещение в качестве первого символа optstring двоеточия является хорошей мыслью, поскольку это позволяет различать «неверную опцию» и «отсутствующий аргумент опции». Расплатой за это является то, что getopt () в этом случае также не предпринимает никаких действий, заставляя вас выводить собственные сообщения об ошибках. Вот предыдущий пример, на этот раз с обработкой ошибок: int ос; /* символ опции */ char *b_opt_arg; while ((ос = getopt(argc, argv, ":ab:")) != -1) { switch (oc) {
2.3. Разбор опций: get opt () и getopt_long () 45 case 'a': /* обработка -а, установка соответствующего флага */ break; case 'b1: /* обработка -b, получение значения аргумента из optarg */ b_opt_arg = optarg; break; case ':': /* отсутствует аргумент опции */ fprintf(stderr, "%s: option 4-%c' requires an argument\n", argv[0], optopt); break; case '?': default: /* недействительная опция */ fprintf(stderr, "%s: option N-%c' is invalid: ignored\n", argv[0], optopt); break; } } Замечание о соглашениях по именованию флагов или опций: в большом количестве кода для Unix используются имена в виде xf lg для любого данного символа опции х (например, nf lg в echo V7; обычным является также xf lag). Это может быть замечательным для автора программы, который без проверки документации знает, что означает опция х. Но это не подходит для кого-то еще, кто пытается прочесть код и не знает наизусть значений всех символов опций. Гораздо лучше использовать имена, передающие смысл опции, как no__newline для опции -n echo. 2.3.2. GNU getopt () и порядок опций Стандартная функция getopt () прекращает поиск опций, как только встречает аргумент командной строки, который не начинается с '-'. GNU getopt () отличается: она просматривает в поисках опций всю командную строку. По мере продвижения она переставляет элементы argv, так что после ее завершения все опции оказываются переставленными в начало, и код, продолжающий разбирать аргументы с argv[optind] до argv[argc-l], работает правильно. Во всех случаях специальный аргумент '--' завершает сканирование опций. Вы можете изменить поведение по умолчанию, использовав в optstring специальный первый символ следующим образом: optstring [0 ] ==''+' GNU getopt () ведет себя, как стандартная getopt (); она возвращает опции по мере их обнаружения, останавливаясь на первом аргументе, не являющемся опцией.
46 Глава 2. Аргументы, опции и переменные окружения Это работает также в том случае, если в окружении присутствует строка POSIXLY_CORRECT. optstring [0] ==•'-' GNU getopt () возвращает каждый аргумент командной строки независимо от того, представляет он аргумент или нет. В этом случае для каждого такого аргумента функция возвращает целое 1, а указатель на соответствующую строку помещаете optarg. Как и для стандартной getopt (), если первым символом optstring является ': \ GNU getopt () различает «неверную опцию» и «отсутствующий аргумент опции», возвращая соответственно '?' или 4: \ Символ ':' в optstring может быть вторым символом, если первым символом является 4+' или '-'. Наконец, если за символом опции в optstring следуют два двоеточия, эта опция может иметь необязательный аргумент. (Быстро повторите это три раза!) Такой аргумент считается присутствующим, если он находится в том же элементе argv, что и сама опция, и отсутствующим в противном случае. В случае отсутствия аргумента GNU getopt () возвращает символ опции, а в optarg записывает NULL. Например, пусть имеем: while ((с = getopt(argc, argv, "ab::")) != 1) для -bYANKEES, возвращаемое значение будет 4b\ a optarg указывает на «YANKEES», тогда как для -Ь или '-b YANKEES' возвращаемое значение будет все то же *Ь\ но в optarg будет помещен NULL. В последнем случае «YANKEES» представляет отдельный аргумент командной строки. 2.3.3. Длинные опции Функция ge topt_long () осуществляет разбор длинных опций в описанном ранее виде. Дополнительная процедура getopt_long_only () работает идентичным образом, но она используется для программ, в которых все опции являются длинными и начинаются с единичного символа '-'. В остальных случаях обе функции работают точно так же, как более простая функция GNU getopt (). (Для краткости, везде, где мы говорим «getopt_long ()», можно было бы сказать «getopt_long () и getopt_long_only () ».) Вот объявления функций из справки getoptQ) GNU/Linux: #include <getopt.h> /* GLIBC */ int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex); int getopt__long_only(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex); Первые три аргумента те же, что и в getopt (). Следующая опция является указателем на массив stuct option, который мы назовем таблицей длинных опцийМ который вскоре опишем. Параметр longindex, если он не установлен в NULL, указывает на
2.3. Разбор опций: getopt () и getopt__long () 47 переменную, в которую помещается индекс обнаруженной длинной опции в longopts. Это полезно, например, при диагностике ошибок. 2.3.3.1. Таблица длинных опций Длинные опции описываются с помощью массива структур struct option. Структура struct option определена в <getopt. h>; она выглядит следующим образом: struct option { const char "*name; int has_arg; int *flag; int val; }; Элементы структуры следующие: const char *name Это имя опции без предшествующих черточек, например, «help» или «verbose», int has__arg Переменная описывает, имеет ли длинная опция аргумент, и если да, какого вида этот аргумент. Значение должно быть одно из представленных в табл. 2.1. Макроподстановки являются некоторыми символическими именами для числовых значений, приведенных в таблице. Хотя числовые значения тоже работают, макроподстановки гораздо легче читать, и вы должны их использовать вместо соответствующих чисел в любом коде, который пишете. int *flag Если этот указатель равен NULL, getopt__long () возвращает значение поля val структуры. Если он не равен NULL, переменная, на которую он указывает, заполняется значением val, a getopt__long () возвращает 0. Если flag не равен NULL, но длинная опция отсутствует, указанная переменная не изменяется. int val Если длинная опция обнаружена, это возвращаемое значение или значение для загрузки в *flag, если flag не равен NULL. Обычно, если flag не равен NULL, val является значением true/false, вроде 1 или 0. С другой стороны, если flag равен NULL, val обычно содержит некоторую символьную константу. Если длинная опция соответствует короткой, эта символьная константа должна быть той же самой, которая появляется в аргументе opt string для этой опции. (Все это станет вскоре ясно, когда мы рассмотрим несколько примеров.) У каждой длинной опции есть один такой элемент с соответствующими заполненными значениями. В последнем элементе массива все значения должны быть равны нулю. Нет необходимости сортировать массив: getopt_long () осуществляет линейный поиск. Однако, сортировка его по длинным именам может упростить его чтение для программиста.
48 Глава 2. Аргументы, опции и переменные окружения Таблица 2.1. Значения для has_arg Макроподстановка no_argument required__argument optional_argument Числовое значение 0 1 2 Смысл Опция не принимает аргумент Опции требуется аргумент Аргумент опции является необязательным При первой встрече использование flag и val кажется сбивающим с толку. Давайте сделаем на время шаг назад и рассмотрим, почему это работает именно таким способом. В большинстве случаев, обработка опций заключается в установке значений различных флаговых переменных при обнаружении различных символов опций, наподобие этого: while ((с = getopt(argc, argv, ":af:hv")) != -1) { switch (с) { case 'a': do_all = 1; break; case 'f': myfile = optarg; break; case 'h': do_help = 1; break; case 'v': do_verbose = 1; break; ... /* Здесь обработка ошибок */ - } } Когда flag не равен NULL, getopt_long () устанавливает значения переменных за вас. Это снижает число операторов case в предыдущем switch с трех до одного. Вот пример таблицы длинных опций и код для работы с ней: int do_all, do_help, do_verbose; /* флаговые переменные */ char *myfile; struct option longopts[] = { { "all", no_argument/ & do_all, 1 }, { "file", required_argument, NULL, 'f1 }, { "help", no_argument, & do_help, 1 }, { "verbose", no_argument, & do_verbose, 1 }, { 0, 0, 0, 0 } }';
2.3. Разбор опций: get opt (¦) и getopt_long () 49 while ((с = getopt_long(argc, argv, " : f: ", longopts, NULL)) != -1) { switch (c) { case 'f': myfile = optarg; break; case 0: /* getopt_long() устанавливает значение переменной, просто продолжить выполнение*/ break; ... /* Здесь обработка ошибок */ } } Обратите внимание, что значение, переданное аргументу opt string, не содержит больше 'a', 'h' или V. Это означает, что соответствующие короткие опции неприемлемы. Чтобы разрешить как длинные, так и короткие опции, вам придется восстановить в switch соответствующие case из первого примера. На практике следует писать свои программы так, чтобы у каждой короткой опции была также соответствующая длинная опция. В этом случае проще всего установить в flag NULL, а в val соответствующий единичный символ. 2.3.3.2. Длинные опции в стиле POSIX Стандарт POSIX резервирует опцию -W для специфических для производителя возможностей. Поэтому по определению -W непереносимо между различными системами. Если за W в аргументе optstring следует точка с запятой (обратите внимание: не двоеточие), getopt_long () рассматривает -Wlongopt так же, как --longopt. Соответственно в предыдущем примере измените вызов следующим образом: while ((с = getopt_long(argc, argv, ":f:W;",longopts, NULL)) != -1) { С этим изменением -Wall является тем же, что и --all, a -Wf ile=myf ile тем же, что --f ile=myf ile. Использование точки с запятой позволяет программе использовать при желании -W в качестве обычной опции. (Например, GCC использует ее как нормальную опцию, тогда как gawk использует ее для совместимости с POSIX.) 2.3.3.3. Сводка возвращаемых значений getopt__long () Теперь должно быть ясно, что getopt_long () предоставляет гибкий механизм для разбора опций. В табл. 2.2 приведена сводка всех возможных возвращаемых значений функции и их значение. Таблица 2.2. Возвращаемые значения getopt_long () Возвращаемый код Значение 0 getopt_long () установила флаг, как указано в таблице длинных опций 1 optarg указывает на простой аргумент командной строки -
50 Глава 2. Аргументы, опции и переменные окружения Таблица 2.2. Возвращаемые значения getopt_long () (Продолжение) Возвращаемый код Значение '?' Недействительная опция ':' Отсутствующий аргумент опции V Символ опции V -1 Конец опций Наконец, мы улучшим предыдущий пример кода, показав оператор switch полностью: int do_all/ do_help, do_verbose; /* флаговые переменные */ char *myfile, *user; /* файл ввода, имя пользователя */ struct option longopts[] = { { "all", no_argument, & do_all, 1 }, { "file", required__argument, NULL, 'f•}, { "help", no__argument, & do_help, 1 }, { "verbose", no_argument, & do_verbose, 1 }, { "user" , optional_argument, NULL, 'u'}, { 0, 0, 0, 0 } }; while((c=getopt_long(argc,argv,":ahvf:u::W;",longopts,NULL)) != -1) { switch (c) { case 'a': do_all = 1; break; case 'f': myfile = optarg; break; case 'h': do_help = 1; break; case 'u': ' if (optarg != NULL) user = optarg; else user = "root"; break; case 'v1: do_verbose = 1; break; case 0: /* getopt_long() установил переменную, просто продолжить */ break; #if 0
2.3. Разбор опций; getopt () и getopt_long() 51 case 1: /* * Используйте этот case, если getopt__long () должна просмотреть ¦*• все аргументы. В этом случае добавьте к optstring ведущий * символ *-*. Действительный код, если он есть, работает здесь. */ break; #endif case ':': /* отсутствует аргумент рпДии */ fprintf(stderr, "%s: option N-%c' requires an argument\n", argv[0], optopt); break; case '?': default: /* недействительная опция */ fprintf(stderr, "%s: option N-%c ' is invalid: ignored\n", argv[0], optopt); break; } } В своих программах вы можете захотеть сделать для каждого символа опции комментарии, объясняющие их значение. Однако, если вы использовали описательные имена переменных для каждого символа опции, комментарии уже не так нужны. (Сравните do^_verbose и vflag.) 2.3.3.4. GNU getopt () или getopt_long в программах пользователей Вы можете захотеть использовать в своих программах GNU getopt () или getopt__long () и заставить их работать на не-Linux системах. Это нормально; просто скопируйте исходные файлы из программы GNU или из CVS архива библиотеки С GNU , (GLIBCL. Исходные файлы getopt.h, getopt. с и getoptl. с. Они лицензированы на условиях меньшей общедоступной лицензии (Lesser General Public License) GNU, которая позволяет включать библиотечные функции даже в патентованные программы. Вы должны включить в свою программу копию файла COPYING. LIB наряду с файлами getopt. h, getopt. с и getoptl. с. Включите исходные файлы в свой дистрибутив и откомпилируйте их с другими исходными файлами. В исходном коде, вызывающем getopt_long(), используйте ^include <getopt ,Ь>\ а не ^include "getopt.h"'. Затем, при компилировании, добавьте к командной строке компилятора С -I. Таким способом сначала будет найдена локальная копия заголовочного файла. Вы можете поинтересоваться: «Вот так, я уже использую GNU/Linux. Почему я должен включать getopt__long() в свой исполняемый модуль, увеличивая его размер, если См. http: //sources . redhat. com. -Примеч. автора.
52 Глава 2. Аргументы, опции и переменные окружения процедура уже находится в библиотеке С?» Это хороший вопрос. Однако, здесь не о чем беспокоиться. Исходный код построен так, что если он компилируется на системе, которая использует GLD3C, откомпилированные файлы не будут содержать никакого кода! Вот подтверждение на нашей системе: $ uname -a /* Показать имя и тип системы */ Linux example 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux $ Is -1 getopt.o getoptl.o /* Показать размеры файлов */ -rw-r—г-- 1 arnold devel 9836 Mar 24 13:55 getopt.o -rw-r—r— 1 arnold devel 10324 Mar 24 13:55 getoptl.o $ size getopt.o getoptl.o /* Показать включенные в исполняемый модуль размеры */ text data bss dec hex filename 0 0 0 0 0 getopt.o 0 0 0 0 0 getoptl.o Команда size печатает размеры различных составных частей двоичного объекта или исполняемого файла. Мы объясним вывод в разделе 3.1 «Адресное пространство Liniix/ Unix». Что важно понять прямо сейчас, это то, что несмотря на ненулевой размер самих файлов, они не вносят никакого вклада в конечный исполняемый модуль. (Думаем, это достаточно ясно.) ч 2.4. Переменные окружения Окружение представляет собой набор пар вида'имя-значение' для каждой программы. Эти пары называются переменными окружения. Каждое имя состоит от одной до любого числа буквенно-цифровых символов или символов подчеркивания ('_'), но имя не может начинаться с цифры. (Это правило контролируется оболочкой; С API может помещать в окружение все, что захочет, за счет возможного запутывания последующих программ.) Переменные окружения часто используются для управления поведением программ. Например, если в окружении существует POSIXLY_CORRECT, многие программы запрещают расширения или историческое поведение, которые несовместимы со стандартом POSIX. Вы можете решить использовать (и должны задокументировать) переменные окружения для управления поведением вашей программы. Например, вы можете вместо аргумента командной строки использовать для опций отладки переменную окружения. Преимуществом использования переменных окружения является то, что пользователи могут установить их в своем загрузочном файле и не беспокоиться больше постоянным набором определенных опций в командной строке. Конечно, недостатком использования переменных окружения является то, что они могут молча изменять поведение программы. Джим Мейеринг (Jim Meyering), сопроводитель Coreutils, выразил это таким образом:
2.4. Переменные окружения 53 Они упрощают пользователю настройку программы без изменения способа ее вызова. Это может быть как благословением, так и проклятием. Если вы пишете сценарий, который зависит от значения определенной переменной окружения, а затем этот сценарий использует еще кто-то, у кого нет таких же установок окружения, он легко может потерпеть неудачу (или, что еще хуже, молча выдать неверные результаты). 2.4.1. Функции управления окружением Несколько функций позволяют получать значения переменных окружения, изменять эти значения или удалять их. Вот соответствующие объявления: #include <stdlib.h> char *getenv(const char *name); /* ISO С: Получить переменную окружения */ int setenv(const char *name, /* POSIX: Установить переменную */ const char *value, /* окружения */ int overwrite); int putenv(char *string); /* XSI: Установить переменную окружения, использует строку */ void unsetenv(const char *name); /* POSIX: Удалить переменную окружения */ int clearenv(void); /* Общее: очистить все окружение */ Функция getenv () -та, которую вы будете использовать в 99% случаев. Ее аргументом является имя переменной окружения, которую нужно искать, такое, как «НОМЕ» или «PATH». Если переменная существует, getenv () возвращает указатель на строковое значение. Если нет, возвращается NULL. Например: char *pathval; /* Поиск PATH; если нет, использовать значение по умолчанию */ if ((pathval = getenv("PATH")) == NULL) pathval = "/bin:/usr/bin:/usr/ucb"; Иногда переменная окружения существует, но с пустым значением. В этом случае возвращаемое значение не равно NULL, но первый символ, на которую оно указывает, будет нулевым байтом, который в С является символом конца строки,' \ 0'. Ваш код должен позаботиться проверить, что возвращаемое значение не равно NULL. Если оно не NULL, необходимо также проверить, что строка не пустая, если вы хотите для чего-то использовать значение переменной. В любом случае, не используйте возвращенное значение слепо. Для изменения переменной окружения или добавления к окружению еще одной используется setenv(): if (setenv("PATH", "/bin:/usr/bin:/usr/ucb" , 1) != 0) { /* обработать ошибку */ } Возможно, что переменная уже существует в окружении. Если третий аргумент равен true (не ноль), новое значение затирает старое. В противном случае, предыдущее значение
54 Глава 2. Аргументы, опции и переменные окружения не меняется. Возвращаемое значение равно -1, если для новой переменной не хватило памяти, и 0 в противном случае, setenv () для сохранения в окружении делает индивидуальные копии как имени переменной, так и нового ее значения. Более простой альтернативой setenv() является putenv(), которая берет одну строку «имя=значение» и помещает ее в окружение: if (putenv( "PATH=/.bin:/usr/Ып:/usr/ucb") != 0) { /* обработать ошибку */ } put env () слепо заменяет любые предшествующие значения для той же переменной. А также, и это, возможно, более важно, строка, переданная putenv (), помещается непосредственно в окружение. Это означает, что если ваш код позже изменит эту строку (например, если это был массив, а не строковая константа), окружение также будет изменено. Это, в свою очередь, означает, что вам не следует использовать в качестве параметров для putenv () локальную переменную. По всем этим причинам setenv () является более предпочтительной функцией. \ЗАМЕЧАНИЕ, GNU putenv () имеет дополнительную (документированную) особенность в своем поведении. Если строка аргумента является именем без следующего за ним символа -, именованная переменная удаляется. Программа GNU I env, которую мы рассмотрим дач ее в этой главе, полагается на такое поведение. Функция unset env () удаляет переменную из окружения: unsetenv("PATH"); Наконец, функция clear env () полностью очищает окружение: if (clearenvO != 0) { /* обработать ошибку */ } Эта функция не стандартизирована POSIX, хотя она доступна в GNU/Linux и нескольких коммерческих вариантах Unix. Ее следует использовать, если приложение должно быть очень безопасным и нужно построить собственное окружение с нуля. Если clearenv () недоступна, в справке GNU/Linux для clearenvC) рекомендуется использовать для выполнения этой задачи'environ = NULL'. 2.4.2. Окружение в целом: environ Правильным способом работы с окружением является использование функций, описанных в предыдущем разделе. Однако, стоит взглянуть на то, как это работает «под капотом». Внешняя переменная environ предоставляет доступ таким же способом, как argv предоставляет доступ к аргументам командной строки. Вы сами должны объявить переменную. Хотя она и стандартизирована POSIX, environ намеренно не объявлена ни в одном стандартном заголовочном файле. (Это, кажется, прослеживается из исторической практики,) Вот объявление: extern char **environ; /* Смотрите, нет заголовочного файла! POSIX */
2.4. Переменные окружения 55 Как и в argv, завершающим элементом environ является NULL. Однако, здесь нет переменной «числа строк окружения», которая соответствовала бы argc. Следующая простая программа распечатывает все окружение: /* ch02-printenv.c Распечатать окружение. */ #include <stdio.h> extern char **environ; int main(int argc, char **argv) { int i; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) printf("%s\n", environ[i]); return 0; } Хотя это и маловероятно, перед попыткой использовать environ эта программа проверяет, что она не равна NULL. Переменные хранятся в окружении в случайном порядке. Хотя некоторые оболочки Unix хранят переменные окружения в отсортированном по именам переменных виде, это формально не требуется, и многие оболочки не сортируют их. В качестве уловки реализации можно получить доступ к окружению, объявив третий параметр main (): int main(int argc, char **argv, char **envp) { } Затем можно использовать envp также, как environ. Хотя это иногда можно увидеть в старом коде, мы не рекомендуем такое использование; environ является официальным, стандартным, переносимым способом получения доступа ко всему окружению, если это вам необходимо. 2.4.3. GNU env Чтобы завершить главу, рассмотрим GNU версию команды env. Эта команда добавляет переменные к окружению в ходе выполнения одной команды. Она может использоваться также для очищения окружения в ходе этой команды или для удаления отдельных переменных окружения. Программа обеспечивает нас двойной функциональностью, поскольку проявляет возможности как getopt_long (), так и несколько других возможностей, обсуждавшихся в этом разделе. Вот как вызывается программа: $ env --help Usage: env [OPTION] ... [-] [NAME=VALUE] . . . [COMMAND [ARG] . . . ] /* Устанавливает соответствующее VALUE для каждого NAME и запускает COMMAND. */ -i, --ignore-environment /* запустить с пустым окружением */
56 Глава 2. Аргументы, опции и переменные окружения -u, --unset=NAME /* удалить переменную из окружения */ --help /* показать этот экран справки и выйти */ --version /* вывести информацию о версии и выйти */ /* Простое - предполагает -i. Если не указана COMMAND, отображает имеющееся окружение. Об ошибках сообщайте в <bug-coreutils@gnu.org>. */ Вот несколько примеров вызовов команды: $ env - щургод argl /* Очистить окружение, запустить программу с args */ $ env - РАТН=/bin:/usr/bin xnyprog argl /* Очистить окружение, добавить PATH, запустить программу */ $ env -u IPS PATH= /bin: /usr/bin myprog argl /* Сбросить IFS, добавить PATH, запустить программу */ Код начинается со стандартной формулировки авторских прав GNU и разъясняющего комментария. Мы для краткости их опустили. (Формулировка авторского права обсуждается в Приложении С «Общедоступная лицензия GNU». Показанного ранее вывода --help достаточно для понимания того, как работает программа.) За объявленным авторским правом и комментарием следуют подключаемые заголовочные файлы и объявления. Вызов макроса 'N_ (" string")' (строка 93) предназначен для использования при локализации программного обеспечения, тема, освещенная в главе 13 «Интернационализация и локализация». Пока вы можете рассматривать его, как содержащий строковую константу. 80 #include <config.h> 81 #include <stdio.h> 82 #include <getopt.h> 83 #include <sys/types.h> 84 #include <getopt.h> 85 86 #include "system.h" 87 #include "error.h" 88 #include "closeout.h" 89 90 /* Официальное имя этой программы (напр., нет префикса Ng'). */ 91 #define PROGRAM__NAME "env" 92 93 #define AUTHORS N_ ("Richard'Mlynarik and David MacKenzie") 94 95 int putenv (); 96 97 extern char **environ; 98 99 /* Имя, посредством которого эта программа была запущена. */ 100 char *program__name; 101
2.4. Переменные окружения 57 102 static struct option const longopts[] = 103 { 104 {"ignore-environment", no_argument, NULL, 'i'}, 105 {"unset", required_argument/ NULL, 'u'}, 106 {GETOPT_HELP_OPTION_DECL}, 107 {GETOPT_VERSION_OPTION_DECL} , 108 {NULL, 0, NULL, 0} 109 }; GNU Coreutils содержит большое число программ, многие из которых выполняют одни и те же общие задачи (например, анализ аргументов). Для облегчения сопровождения многие типичные идиомы были определены в виде макросов. Двумя таким макросами являются GETOPT_HELP_OPTION_DECL и GETOPT_VERSION_OPTION (строки 106 и 107). Вскоре мы рассмотрим их определения. Первая функция, usage (), выводит информацию об использовании и завершает программу. Макрос _ (" string") (строка 115, используется также по всей программе) также предназначен для локализации, пока также считайте его содержащим строковую константу. • 111 void 112 usage (int status) 113 { 114 if (status != 0) 115 fprintf(stderr,_("Try 4%s --help' for more information.\n") , 116 program_name); 117 else 118 { 119 printf (_("\ 120 Usage: %s [OPTION]... [-] [ NAME=VALUE] .. . [COMMAND [ARG] . . . ] \n") , 121 program_name); 122 fputs (_("\ 123 Set each NAME to VALUE in the environment and run COMMAND. \n\ 124 \n\ 125 -i, --ignore-environment start with an empty environment\n\ 126 -u, --unset=NAME remove variable from the environment\n\ 127 "), stdout); 128 fputs (HELP_OPTION_DESCRIPTION, stdout); 129 fputs (VERSION_OPTION_DESCRIPTION, stdout); 130 fputs (_("\ 131 \n\ 132 A mere - implies -i. If no COMMAND, print the resulting\ 133 environment.\n"), stdout); 134 printf (_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); 135 } 136 exit (status); 137 }
58 Глава 2. Аргументы, опции и переменные окружения Первая часть main () объявляет переменные и настраивает локализацию. Функции set locale (), b i ndt ex t domain () и textdomain () (строки 147-149) обсуждаются в главе 13 «Интернационализация и локализация». Отметим, что эта программа использует аргумент main () envp (строка 140). Это единственная программа Coreutils, которая так делает. Наконец, вызов atexitO в строке 151 (см. раздел 9.1.5.3. «Функции завершения») регистрирует библиотечную функцию Coreutils, которая очищает все выходные буферы и закрывает stdout, выдавая сообщение при ошибке. Следующая часть программы обрабатывает аргументы командной строки, используя getopt_long (). 139 int 140 main (register int argc, register char **argv, char **envp) 141 { 142 char *dummy_environ[1]; 143 int optc; 144 int ignore_environment = 0;v 145 146 progranwiame = argv[0] ; 147 setlocale (LC__ALL, ""); 148 bindtextdomain (PACKAGE, LOCALEDIR); 149 textdomain (PACKAGE'); 150 151 atexit (close_stdout); 152 153 while((optc = getopt_long(argc,argv,"+iu:",longopts,NULL))!=-1) 154 { 155 switch (optc) 156 { 157 case 0: 158 break; 159 case 'i': 160 ignore_environment = 1; 161 break; 162 case 'u': 163 break; 164 case_GETOPT_HELP_CHAR; 165 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS); 166 default: 167 usage B); 168 } 169 } 170 171 if (optind != argc && istrcmp (argv[optind] , "-,I)) 172 ignore_environment =1;
2.4. Переменные окружения 59 Вот отрывок из файла src / sys2 . h в дистрибутиве Coreutils с упомянутыми ранее определениями и макросом 'case_GETOPT_xxx\ использованным выше (строки 164-165): /* Вынесение за скобки общей части кода, обрабатывающего --help и -- version. */ /* Эти значения перечисления никак не могут конфликтовать со значениями опций, обычно используемыми командами, включая CHAR__MAX + 1 и т.д. Избегайте CHAR_MIN - 1, т.к. оно может равняться -1, значение завершения опций getopt. */ enum { GETOPT_HELP_CHAR = (CHAR_MIN - 2), GETOPT_VERSION_CHAR = (CHAR_MIN - 3) }; #define GETOPT_HELP_OPTION_DECL \ "help", no_argument, 0, GETOPT_HELP_CHAR #define GETOPT_VERSION_OPTION_DECL \ "version", no_argument, 0, GETOPT_VERSION_CHAR #define case_GETOPT_HELP_CHAR \ case GETOPT_HELP_CHAR: \ usage (EXIT_SUCCESS); \ break; #define case_GETOPT_VERSION_CHAR(Program_name, Authors) \ case GETOPT_VERSION_CHAR: \ version_etc (stdout, Program.name, PACKAGE, VERSION, Authors); \ exit (EXIT_SUCCESS); \ break; Результатом этого кода является печать сообщения об использовании утилиты для --help и печать информации о версии для --version. Обе опции завершаются успешно. («Успешный» и «неудачный» статусы завершения описаны в разделе 9.1.5.1. «Определение статуса завершения процесса».) Поскольку в Coreutils входят десятки утилит, имеет смысл вынести за скобки и стандартизовать как можно больше повторяющегося кода. Возвращаясь к env. с: 174 environ = dummy_environ; 17 5 environ[0] = NULL; 176 177 if ( ! i^nore_environrnent) 178 for (; *envp; envp++) 179 putenv (*envp); 180 181 optind =0; /* Принудительная реинициализация GNU getopt. */ 182 while((optc = getopt_long(argc,argv,"+iu:",longopts,NULL)) != -1)
60 Глава 2. Аргументы, опции и переменные окружения 183 if (optc == 'u') 184 putenv (optarg); /* Требуется GNU putenv. */ 185 186 if(optind!=argc && !strcmp(argv[optind],"-"))/*Пропустить опции*/ 187 ++optind; 188 189 while (optind < argc && strchr (argv[optind], '=')) /* Установить переменные окружения */ 190 putenv (argv[optind++]); 191 192 /* Если программа не указана, напечатать переменные окружения и выйти. */ 193 if (optind == argc) 194 { 195 while (*environ) 196 puts (*environ++); 197 exit (EXI'T_SUCCESS) ; 198 } Строки 174-179 переносят существующие переменные в новую копию окружения. В глобальную переменную environ помещается указатель на пустой локальный массив. Параметр envp поддерживает доступ к первоначальному окружению. Строки 181-184 удаляют переменные окружения, указанные в опции -и. Программа осуществляет это, повторно сканируя командную строку и удаляя перечисленные там имена. Удаление переменных окружения основывается на обсуждавшейся ранее особенности GNU putenv (): при вызове с одним лишь именем переменной (без указанного значения) putenv () удаляет ее из окружения. После опций в командной строке помещаются новые или замещающие переменные окружения. Строки 189-190 продолжают сканирование командной строки, отыскивая установки переменных окружения в виде *имя=значение\ По достижении строки 192, если в командной строке ничего не осталось, предполагается, что env печатает новое окружение и выходит из программы. Она это и делает (строки 195-197). Если остались аргументы, они представляют имя команды, которую нужно вызвать, и аргументы для передачи этой новой команде. Это делается с помощью системного вызова execvp () (строка 200), который замещает текущую программу новой. (Этот вызов обсуждается в разделе 9.1.4 «Запуск новой программы: семейство exec () »; пока не беспокойтесь о деталях.) Если этот вызов возвращается в текущую программу, он потерпел неудачу. В таком случае env выводит сообщение об ошибке и завершает программу. 200 execvp (argv[optind], &argv[optind]); 201 202 { 203 int exit_status = (errho == ENOENT ? 127 : 126); 2 04 error @, errno, "%s", argv[optind]);
2.5. Резюме 61 205 exit (exit_status); 206 } 207 > Значения кода завершения 12 6 и 127 (определяемые в строке 203) соответствуют стандарту POSIX. 127 означает, что программа, которую execvp () попыталась запустить, не существует. (ENOENT означает, что файл не содержит записи в каталоге.) 12 6 означает, что файл существует, но была какая-то другая ошибка. 2.5. Резюме Программы на С получают аргументы своей командной строки через параметры argc и argv. Функция getopt () предоставляет стандартный способ для последовательного разбора опций и их аргументов. GNU версия getopt () предоставляет некоторые расширения, a getopt_long () и getopt_J.ong_only () дает возможность легкого разбора длинных опций. Окружение представляет собой набор пар 'имя=значение\ который каждая программа наследует от своего родителя. Программы могут по прихоти своего автора использовать для изменения своего поведения переменные окружения, в дополнение к любым аргументам командной строки. Для получения значений переменных окружения, изменения их значений или удаления существуют стандартные процедуры (getenv (), setenv (), putenv () и unsetenv ()). При необходимости можно получить доступ ко всему окружению через внешнюю переменную environ или через третий аргумент char **envp функции main (). Последний способ не рекомендуется. Упражнения 1. Предположим, что программа принимает опции -а, -Ь и -с, и что -Ь требует наличия аргумента. Напишите для этой программы код ручного разбора аргументов без использования getopt () или getopt_long (). Для завершения обработки опций принимается --. Убедитесь, что -ас работает, также, как -bYANKEES, -b YANKEES и -abYANKEES. Протестируйте программу. 2. Реализуйте getopt (). Для первой версии вы можете не беспокоиться насчет случая 'optstring[0] == ':". Можете также игнорировать opt err. 3. Добавьте код для'optstring[0] == Vnopterr к своей версии getopt О . 4. Распечатайте и прочтите файлы GNU getopt. h, getopt. с и getoptl. с. 5. Напишите программу, которая объявляет как environ, так и envp, и сравните их значения. 6. Разбор аргументов командной строки и опций является тем колесом, которое многие люди не могут не изобретать вновь. Вы можете захотеть познакомиться с различными анализирующими аргументы пакетами, помимо getopt () и getopt_long(), такими, как:
62 Глава 2. Аргументы, опции и переменные окружения • библиотека анализа аргументов Plan 9 From Bell Labs argBM, • Argp6, • Argv7, • Autoopts , • GNU Gengetopt9, • Opt10, • Popt11. См. также справочную страницу poptC) системы GNU/Linux. 7. Дополнительный балл: почему компилятор С не может полностью игнорировать ключевое слово regi s ter? Подсказка: какие действия невозмоэюно совершать с регистровой переменной? http : //plan9 .bell-labs . com/magic/man2html/2/arg -Примеч. автора. http : //www. gnu. org/manual/glibc/html_node/Argp.html -Примеч. автора. 7 http: //256 . com/ sources/ argv -Примеч. автора. http://autogen.sourQeforge.net/autoopts.html-npuMe4.aemopa. 9 ftp : / / ftp . gnu. org/gnu/gengetopt/ - Примеч. автора. 10 http: //nis-www. lanl .gov/~jt/Sof tware/opt/opt-3 .19 .tar .gz- Примеч. автора. http : //freshmeat .net/projects/popt/?topic_id=809 -Примеч. автора
Глава 3 Управление памятью на уровне пользователя В этой главе: • 3.1. Адресное пространство Linux/Unix 63 • 3.2. Выделение памяти 67 • 3.3. Резюме 89 • Упражнения 90 1>ез памяти для хранения данных программа не может выполнить никакую работу. (Или, скорее, невозможно выполнить никакую полезную работу.) Реальные программы не могут позволить себе полагаться на буферы и массивы структур данных фиксированного размера. Они должны быть способны обрабатывать вводимые данные различных размеров, от незначительных до больших. Это, в свою очередь, ведет к использованию динамически выделяемой памяти - памяти, выделяемой в ходе исполнения, а не при компиляции. Вот как вводится в действие принцип GNU «никаких произвольных ограничений». Поскольку динамически выделяемая память является основным строительным блоком для реальных программ, мы рассмотрим этот вопрос в начале, до рассмотрения всего остального. Наше обсуждение фокусируется на рассмотрении процесса и его памяти исключительно на уровне пользователя; оно не имеет ничего общего с архитектурой процессора. 3.1. Адресное пространство Linux/Unix В качестве рабочего определения мы приняли, что процесс является запущенной программой. Это означает, что операционная система загрузила исполняемый файл для этой программы в память, сделала доступными аргументы командной строки и переменные окружения и запустила ее. Процесс имеет пять выделенных для него концептуально различных областей памяти: Код Часто называемая сегментом текста область, в которой находятся исполняемые инструкции. Linux и Unix организуют вещи таким образом, что несколько запущенных экземпляров одной программы по возможности разделяют свой код; в любое
64 Глава 3. Управление памятью на уровне пользователя время в памяти находится лишь одна копия инструкций одной и той же программы. (Это прозрачно для работающих программ.) Часть исполняемого файла, содержащая сегмент текста, называется секцией текста. Инициализированные данные Статически выделенные и глобальные данные, которые инициализированы ненулевыми значениями, находятся в сегменте данных. У каждого процесса с одной и той же запущенной программой свой собственный сегмент данных. Часть исполняемого файла, содержащая сегмент данных, является секцией данных. Инициализированные нулями данные Глобальные и статически выделенные данные, которые по умолчанию инициализированы нулями, хранятся в области процесса, который называют областью BSS2. У каждого процесса, в котором запущена одна и та же программа, своя область BSS. При запуске данные BSS помещаются в сегмент данных. В исполняемом файле они хранятся в секции BSS. Формат исполняемого файла Linux/Unix таков, что пространство исполняемого файла на диске занимают лишь переменные, инициализированные ненулевыми значениями. Поэтому большой массив, объявленный как 'static char somebuf [2048] который автоматически заполняется нулями, не занимает 2 Кб пространства на диске. (Некоторые компиляторы имеют опции, позволяющие вам помещать инициализированные нулями данные в сегмент данных.) Куча (heap) Куча является местом, откуда выделяется динамическая память (получаемая с помощью функции malloc () и подобными ей). Когда из кучи выделяется память, адресное пространство процесса растет, что вы можете заметить, отслеживая запущенный процесс с помощью команды ps. Хотя память можно вернуть обратно системе и сократить адресное пространство процесса, этого почти никогда не происходит. (Мы различаем освобождение больше не использующейся динамической памяти и сокращение адресного пространства; подробнее это обсуждается далее в этой главе.) Для кучи характерен «рост вверх». Это означает, что последовательные элементы, добавляемые к куче, добавляются по адресам, численно превосходящим предыдущие. Куча обычно начинается сразу после области BSS сегмента данных. Существует также другое название для этой области данных —Неинициализированные данные. - Примеч. науч. ред. BSS означает 'Block Started by Symbol', мнемоника из ассемблера IBM 7094. - Примеч. автора.
3.1. Адресное пространство Linux/Unix 65 Стек Сегмент стека - это область, в которой выделяются локальные переменные. Локаль- * ными являются все переменные, объявленные внутри левой открывающей фигурной скобки тела функции (или другой левой фигурной скобки) и не имеющие ключевого слова static. В большинстве архитектур параметры функций также помещаются в стек наряду с «невидимой» учетной информацией, генерируемой компилятором, такой, как возвращаемое функцией значение и адрес возврата для перехода из функции к месту, откуда произошел вызов. (В некоторых архитектурах для этого используются регистры.) Именно использование стека для параметров функций и возвращаемых ими значений делает удобным написание рекурсивных функций (тех, которые вызывают сами себя). Переменные, хранящиеся в стеке, «исчезают», когда функция, их содержащая, возвращается; пространство стека используется повторно для последующих вызовов функций. В большинстве современных архитектур стек «растет вниз», это означает, что элементы, находящиеся глубже в цепи вызова, находятся по численно меньшим адресам. В работающей программе области инициализированных данных, BSS и кучи обычно размещаются в единой протяженной области: сегменте данных. Сегменты стека и кода отделены от сегмента данных и друг от друга. Это показано на рис. 3.1. Хотя перекрывание стека и кучи теоретически возможно, операционная система предотвращает этот случай, и любая программа, пытающаяся это сделать, напрашивается на неприятности. Это особенно верно для современных систем, в которых адресные пространства большие и интервал между верхушкой стека и концом кучи значителен. Различные области памяти могут иметь различную установленную на память аппаратную защиту. Например, сегмент текста может быть помечен «только для исполнения», тогда как у сегментов данных и стека разрешение на исполнение может отсутствовать. Такая практика может предотвратить различные виды атак на безопасность. Подробности, конечно, специфичны для оборудования и операционной системы, и они могут со временем меняться. Стоит заметить, что стандартные как С, так и C++ позволяют размещать элементы с атрибутом const в памяти только для чтения. Сводка взаимоотношений различных сегментов приведена в табл. 3.1. Программа size распечатывает размеры в байтах каждой из секций text, data и BSS вместе с общим размером в десятичном и шестнадцатеричном виде. (Программа спОЗ- memaddr.c показана далее в этой главе; см. раздел 3.2.5 «Исследование адресного пространства».) $ ее -О eh03-memaddr.c -о ch03-memaddr /* Компилировать программу */ $ Is -1 eh03-memaddr /^Показать общий размер */ -rwxr-xr-x 1 arnold devel 12320 Nov 24 16:45 ch03-memaddr $ size ch03-memaddr /^Показать размеры компонентов */ text data bss dec hex filename 1458 276 8 1742 6ce ch03-memaddr 3-159
66 Глава 3. Управление памятью на уровне пользователя Стек программы Старшие адреса СЕГМЕНТ СТЕКА Стек растет вниз Возможная "дыра" в адресном пространстве Куча BSS: заполненные нулем переменные Глобальные и статическ переменные (данные) Куча растет вверх СЕГМЕНТ ДАННЫХ Младшие адреса Исполняемый код | СЕГМЕНТ ТЕКСТА (общий) ш Рис. 3.1. Адресное пространство Linux/Unix Таблица 3.1. Сегменты исполняемой программы и их размещение Память программы Код Инициализированные данные BSS Куча Стек Сегмент адресного пространства Text Data Data Data Stack Секция исполняемого файла Text Data BSS $ strip ch03-memaddr /*Удалить символы */ $ Is -1 ch03-xnamaddr /* Снова показать общий размер */ -rwxr-xr-x 1 arnold devel 3480 Nov 24 16:45 ch03-memaddr $ size ch03-memaddr /*Размеры компонентов не изменились */ text data bss dec hex filename 1458 276 8 1742 6ce ch03-memaddr
3.2. Выделение памяти 67 Общий размер загруженного в память из файла в 12 320 байтов всего лишь 1742 байта. Большую часть этого места занимают символы (symbols), список имен переменных и функций программы. (Символы не загружаются в память при запуске программы.) Программа strip удаляет символы из объектного файла. Для большой программы это может сохранить значительное дисковое пространство ценой невозможности отладки дампа ядра3, если таковой появится. (На современных системах об этом не стоит беспокоиться; не используйте strip.) Даже после удаления символов файл все еще больше, чем загруженный в память образ, поскольку формат объектного файла содержит дополнительные данные о программе, такие, как использованные разделяемые библиотеки, если они есть4. Наконец, упомянем потоки (threads), которые представляют несколько цепочек исполнения в рамках единственного адресного пространства. Обычно у каждого потока имеется свой собственный стек, а также способ получения локальных данных потока, т.е. динамически выделяемых данных для персонального использования этим потоком. Мы больше не будем рассматривать в данной книге потоки, поскольку это является продвинутой темой. 3.2. Выделение памяти Четыре библиотечные функции образуют основу управления динамической памятью С. Мы опишем сначала их, затем последуют описания двух системных вызовов, поверх которых построены эти библиотечные функции. Библиотечные функции С, в свою очередь, обычно используются для реализации других выделяющих память библиотечных функций и операторов C++ new и delete. Наконец, мы обсудим функцию, которую часто используют, но которую мы не рекомендуем использовать. 3.2.1. Библиотечные вызовы: malloc (), calloc (), reallocO, free() Динамическую память выделяют с помощью функций malloc () или callocO. Эти функции возвращают указатели на выделенную память. Когда у вас есть блок памяти определенного первоначального размера, вы можете изменить его размер с помощью функции realloc (). Динамическая память освобождается функцией free (). Отладка использования динамической памяти сама по себе является важной темой. Инструменты для этой цели мы обсудим в разделе 15.5.2 «Отладчики выделения памяти». Дамп ядра (core dump) является образом запущенного процесса в памяти, который создается при неожиданном завершении процесса. Позже этот дамп может быть использован для отладки. Unix-системы называют этот файл core, а системы GNU/Linux - core.pid, где pid - ID потерпевшего крушение процесса. - Примеч. автора. Описание здесь намеренно упрощено. Запущенные программы занимают значительно больше места, чем указывает программа size, поскольку разделяемые библиотеки включены в адресное пространство. Также, сегмент данных будет расти по мере выделения программой памяти. - Примеч. автора.
68 Глава 3. Управление памятью на уровне пользователя 3.2.1.1. Исследование подробностей на языке С Вот объявления функций из темы справки GNU/Linux mallocC): #include <stdlib.h> /* ISO С*/ void *calloc (size__t nmemb, size_t size); /*Выделить и инициализировать нулями */ void *malloc(size_t size); /* Выделить без инициализации */ void free(void *ptr); /* Освободить память */ void *realloc(void *ptr, size_t size); ,/* Изменить размер выделенной памяти */ ' Функции выделения памяти возвращают тип void *. Это бестшовый или общий указатель, все, что с ним можно делать - это привести его к другому типу и назначить типизированному указателю. Примеры впереди. Тип size_t является беззнаковым целым типом, который представляет размер памяти. Он используется для динамического выделения памяти, и далее в книге мы увидим множество примеров его использования. На большинстве современных систем size_t является unsigned long, но лучше явно использовать size_t вместо простого целого типа unsigned. Тип ptrdif f_t используется для вычисления адреса в арифметике указателей, как в случае вычисления указателя в массиве: #define MAXBUF ... char *p; char buf[MAXBUF]; ptrdiff_t where; p = buf; while (/* некоторое условие */) { p += something ; where.= p - buf; /* какой у нас индекс? */ } Заголовочный файл <stdlib.H> объявляет множество стандартных библиотечных функций С и типов (таких, как size_t), он определяет также константу препроцессора NULL, которая представляет «нуль» или недействительный указатель. (Это нулевое значение, такое, как 0 или ' ( (void *) 0)'. Явное использование 0 относится к стилю С-и-; в С, однако, NULL является предпочтительным, мы находим его гораздо более читабельным для кода С.) 3.2.1.2. Начальное выделениепамяти:malloc () Сначала память выделяется с помощью malloc (). Передаваемое функции значение является общим числом затребованных байтов. Возвращаемое значение является указателем на вновь выделенную область памяти или NULL, если память выделить невозможно. В послед-
3.2. Выделение памяти 69 нем случае для обозначения ошибки будет установлен errno. (errno является специальной переменной, которую системные вызовы и библиотечные функции устанавливают для указания произошедшей ошибки. Она описывается в разделе 4.3 «Определение ошибок».) Например, предположим, что мы хотим выделить переменное число некоторых структур. Код выглядит примерно так: struct coord { /* 3D координаты */ int X, у,. z; } *coordinates; unsigned int count; /* сколько нам нужно */ size__t amount; /* общий размер памяти */ /* ... как-нибудь определить нужное число... */ amount = count * sizeof(struct coord); /* сколько байт выделить */ coordinates = (struct coord *) malloc(amount); /* выделить память */ if (coordinates == NULL) { /* сообщить об ошибке, восстановить или прервать */ } /* ... использовать координаты... */ Представленные здесь шаги являются стереотипными. Порядок следующий: 1. Объявить указатель соответствующего типа для выделенной памяти. 2. Вычислить размер выделяемой памяти в байтах. Для этого нужно умножить число нужных объектов на размер каждого из них. Последний получается с помощью оператора С sizeof, который для этой цели и существует (наряду с другими). Таким образом, хотя размер определенной структуры среди различных компиляторов и архитектур может различаться, sizeof всегда возвращает верное значение, а исходный код остается правильным и переносимым. При выделении массивов для строк символов или других данных типа char нет необходимости умножения на sizeof (char), поскольку последнее по определению всегда равно 1. Но в любом случае это не повредит. 3. Выделить память с помощью malloc (), присвоив возвращаемое функцией значение переменной указателя. Хорошей практикой является приведение возвращаемого malloc () значения к типу переменной, которой это значение присваивается. В С этого не требуется (хотя компилятор может выдать предупреждение). Мы настоятельно рекомендуем всегда приводить возвращаемое значение. Обратите внимание, что на C++ присвоение значения указателя одного типа указателю другого типа требует приведения типов, какой бы ни был контекст. Для управления динамической памятью программы C++ должны использовать new и delete, а не malloc () и free (), чтобы избежать проблем с типами. 4. Проверить возвращенное значение. Никогда не предполагайте, что выделение памяти было успешным. Если выделение памяти завершилось неудачей, malloc () возвращает NULL. Если вы используете значение без проверки, ваша программа может быть немедленно завершена из-за нарушения сегментации (segmentation violation), которое является попыткой использования памяти за пределами своего адресного пространства.
70 Глава 3. Управление памятью на уровне пользователя Если вы проверите возвращенное значение, вы можете по крайней мере выдать диагностическое сообщение и корректно завершить программу. Или можете попытаться использовать какой-нибудь другой способ восстановления. Выделив блок памяти и установив в coordinates указатель на него, мы можем затем интерпретировать coordinates как массив, хотя он в действительности указатель: int cur_x, cur_y, cur_z; size_t an_index; an_index = something; cur_x = coordinates[an_index].x; cur_y = coordinates[an_index].y; cur_z = coordinates[an_index].z; Компилятор создает корректный код для индексирования через указатель при получении доступа к членам структуры coordinates [an_index]. ХЗАМЕЧАНИЕ. Блок памяти, возвращенный malloc (),. не инициализирован. Он мо- \жет содержать любой случайный мусор. Необходимо сразу же инициализировать I память нужными значениями или хотя бы нулями. В последнем случае используйте {функцию memset () (которая обсуждается в разделе 12.2 «Низкоуровневая па- Хмять: функции memXXX ()): ¦memset(coordinates, x\0', amount); IДругой возможностью является использование call ос (), которая вскоре будет I описана. Джефф Колье (Geoff Collyer) рекомендует следующую методику для выделения памяти: some_type *pointer; pointer = malloc(count * sizeof(*pointer)); Этот подход гарантирует, что malloc () выделит правильное количество памяти без необходимости смотреть объявление pointer. Если тип pointer впоследствии изменится, оператор sizeof автоматически гарантирует, что выделяемое число байтов остается правильным. (Методика Джеффа опускает* приведение типов, которое мы только что обсуждали. Наличие там приведения типов также гарантирует диагностику, если тип pointer изменится, а вызов malloc () не будет обновлен.) 3.2.1.3. Освобождение памяти: free () Когда вы завершили использование памяти, «верните ее обратно», используя функцию free (). Единственный аргумент является указателем, предварительно полученным с использованием другой функции выделения. Можно (хотя это бесполезно) передать функции free () пустой указатель: free(coordinates); coordinates = NULL; /* не требуется, но хорошая мысль */ После вызова free (coordinates) доступ к памяти, на которую указывает coordinates, запрещен. Она теперь «принадлежит» процедурам выделения, и они могут
3.2. Выделение памяти 71 поступать с ней как сочтут нужным. Они могут изменить содержимое памяти или даже удалить ее из адресного пространства процесса! Таким образом, есть несколько типичных ошибок, которых нужно остерегаться при использовании free () : Доступ к освобожденной памяти Если она не была освобождена, переменная coordinates продолжает указывать на блок памяти, который больше не принадлежит приложению. Это называется зависшим указателем (danglingpointer). На многих системах вы можете уйти от наказания, продолжая использовать эту память, по крайней мере до следующего выделения или освобождения памяти. На других системах^ однако, такой доступ не будет работать. В общем, доступ к освобожденной памяти является плохой мыслью: это непереносимо и ненадежно, и GNU Coding Standards отвергает его. По этой причине неплохо сразу же установить в указателе программы значение NULL. Если затем вы случайно попытаетесь получить доступ к освобожденной памяти, программа немедленно завершится с ошибкой нарушения сегментации (надеемся, до того, как вы успели вашу программу выпустить в свет). Освобождение одного и того же указателя дваэюды Это создает «неопределенное поведение». После передачи блока памяти обратно выделяющим процедурам они могут объединить освобожденный блок с другой свободной памятью, которая есть в их распоряжении. Освобождение чего-то уже освобожденного ведет к неразберихе и в лучшем случае к крушению; известно, что так называемые двойные освобождения приводили к проблемам безопасности. Передача указателя, полученного не от функций malloc () , calloc () или realloc () Это кажется очевидным, но тем не менее важно. Плоха даже передача указателя на адрес где-то в середине динамически выделенной памяти: free(coordinates + 10); /^Освободить все кроме первых 10 элементов */ Этот вызов не будет работать и, возможно, приведет к пагубным последствиям, таким как крушение. (Это происходит потому, что во многих реализациях malloc () «учетная» информация хранится перед возвращенными данными. Когда free () пытается использовать эту информацию, она обнаружит там недействительные данные. В других реализациях, где учетная информация хранится в конце выделенного блока; возникают те же проблемы.) Выход за пределы буфера Доступ к памяти за пределами выделенного блока также ведет к неопределенному поведению, опять из-за того, что она может содержать учетную информацию или, возможно, вообще не принадлежать адресному пространству процесса. Запись в такой участок памяти гораздо хуже, поскольку это может уничтожить учетные данные.
72 Глава 3. Управление памятью на уровне пользователя Отказ в освобождении памяти Любая динамическая память, которая больше не нужна, должна быть освобождена. В частности, необходимо тщательно управлять памятью и освобождать ее, когда она выделяется внутри циклов или рекурсивных или глубоко вложенных вызовов функций. Отказ от этого ведет к утечкам памяти, при которых память процесса может неограниченно расти; в конце концов, процесс завершается из-за нехватки памяти. Эта ситуация может быть особенно разрушительной, если память выделяется для ввода записи или как-то еще связана с вводом: утечка памяти будет незаметна при использовании незначительных объемов ввода, но внезапно станет очевидной (и приведет в замешательство) при больших. Эта ошибка еще хуже для систем, которые должны работать непрерывно, как в системах телефонных коммутаторов. Утечка памяти, вызывающая крушение такой системы, может привести к значительным денежным или другим потерям. Даже если программа никогда не завершается из-за недостатка памяти, постоянно увеличивающиеся программы теряют производительность, поскольку операционная система должна сохранять использующиеся данные в физической памяти. В худшем случае, это может привести к поведению, известному как пробуксовка (thrashing), при которой операционная система так занята перекачкой содержимого адресного пространства в и из физической памяти, что реальная работа не делается. Хотя free () может вернуть освобожденную память системе и сократить адресное пространство процесса, это почти никогда не делается. Вместо этого освобожденная память готова для нового выделения при следующем вызове malloc (), calloc () или realloc(). При условии, что освобожденная память продолжает оставаться в адресном пространстве процесса, стоит обнулить ее перед освобождением. Например, такой способ может оказаться предпочтительным для программ с повышенными требованиями к безопасности. Обсуждение ряда полезных инструментов для отладки динамической памяти см. в разделе 15.5.2 «Отладчики выделения памяти». 3.2.1.4. Изменение размера: reall ос() Динамическая память имеет существенное преимущество перед статически объявленными массивами, поскольку это позволяет использовать столько памяти, сколько нужно, и не больше. Не нужно объявлять глобальный, статический или локальный массив фиксированного размера и надеяться, что он: (а) достаточно большой и (б) не слишком большой. Вместо этого можно выделить ровно столько, сколько нужно, не больше и не меньше. Вдобавок, можно изменять размер динамически выделенной области памяти. Хотя можно сократить размер блока памяти, обычно его увеличивают. Изменение размера осуществляется с помощью realloc(). Продолжая пример с coordinates, типичный код выглядит следующим образом:
3.2. Выделение памяти 73 int new_count; size_t new_amount; struct coord *newcoords; /* установить, например: */ new_count = count * 2; /* удвоить размер памяти */ new_amount = new_count * sizeof(struct coord); newcoords = (struct coord *) realloc(coordinates, new_amount); if (newcoords == NULL) { /* сообщить об ошибке, восстановить или прервать */ } coordinates = newcoords; /* продолжить использование coordinates ... */ Как и в случае с malloc (), шаги стереотипны по природе и сходны по идее: 1. Вычислить новый выделяемый размер в байтах. 2. Вызвать realloc () с оригинальным указателем, полученным от malloc () (или от calloc () или предыдущего вызова realloc ()) и с новым размером. 3. Привести тип и присвоить возвращенное realloc () значение. Подробнее обсудим дальше. 4. Как и для malloc (), проверить возвращенное значение, чтобы убедиться, что оно не равно NULL. Вызов любой функции выделения памяти может завершиться неудачей. При увеличении размера блока памяти realloc () часто выделяет новый блок нужного размера, копирует данные из старого блока в новый и возвращает указатель уже на новый блок. При сокращении размера блока данных realloc () часто обновляет внутреннюю учетную информацию и возвращает тот же указатель. Это избавляет от необходимости копировать первоначальные данные. Однако, если это случится, не думайте, что можно использовать память за пределами нового размера! В любом случае вы можете предположить, что если realloc () не возвращает NULL, старые данные были скопированы для вас в новый участок памяти. Более того, старый указатель больше недействителен, как если бы вы вызвали free () с ним, и использовать его больше не следует. Это верно для всех указателей на этот блок данных, а не только для того, который использовался при вызове free (). Возможно, вы заметили, что в нашем примере для указания на измененный блок памяти использовалась отдельная переменная. Можно было бы (хотя это плохая идея) использовать ту же самую переменную, как здесь: coordinates = realloc(coordinates, new_amount); Это плохо по следующей причине. Когда realloc () возвращает NULL, первоначальный указатель все еще действителен; можно безопасно продолжить использовать эту память. Но если вы повторно используете ту же самую переменную и realloc () возвращает NULL, вы теряете указатель на первоначальную память. Эту память больше нельзя использовать. Что еще важнее, эту память невозможно освободить! Это создает утечку памяти, которую нужно избежать.
74 Глава 3. Управление памятью на уровне пользователя Для версии realloc () в стандартном С есть некоторые особые случаи: когда аргумент ptr равен NULL, realloc () действует подобно malloc () и выделяет свежий блок памяти. Когда аргумент size равен 0, realloc () действует подобно free () и освобождает память, на которую указывает ptr. Поскольку (а) это может сбивать с толку и (б) более старые системы не реализуют эту возможность, мы рекомендуем использовать malloc (), когда вы имеете в виду malloc (), и free (), когда вы имеете в виду free (). Вот другой довольно тонкий момент5. Рассмотрим процедуру, которая содержит статический указатель на динамически выделяемые данные, которые время от времени должны расти. Процедура может содержать также автоматические (т.е. локальные) указатели на эти данные. (Для краткости, мы опустим проверки ошибок. В коде продукта не делайте этого.) Например: void manage_table(void) { static struct table *table; struct table *cur, *p; int i; size_t count; table = (struct table *) malloc(count * sizeof(struct table)); /* заполнить таблицу */. cur = & table[i]; /* указатель на i-й элемент */ cur->i = j; /* использование указателя */ if ¦(/* некоторое условие */) { /* нужно увеличить таблицу */ count += count/2; р = (struct table *)realloc(table,count * sizeof(struct table)); table = p; } cur->i = j; /* ПРОБЛЕМА 1: обновление элемента таблицы */ other_routine(); /* ПРОБЛЕМА 2: см. текст */ cur->j = k; /* ПРОБЛЕМА 2: см. текст */ } Это выглядит просто; manage_table () размещает данные, использует их, изменяет размер и т«д. Но есть кое-какие проблемы, которые не выходят за рамки страницы (или экрана), когда вы смотрите на этот код. В строке, помеченной 'ПРОБЛЕМА 1', указатель cur используется для обновления элемента таблицы. Однако, cur был инициализирован начальным значением table. Если некоторое условие верно и realloc () вернула другой блок памяти, cur теперь Он получен из реальной практики работы с gawk. - Примеч. автора.
3.2. Выделение памяти 75 указывает на первоначальный, освобожденный участок памяти! Каждый раз, когда table меняется, нужно обновить также все указатели на этот участок памяти. Здесь после вызова realloc () и переназначения table недостает строки 'cur = & table [ i] Две строки, помеченные 'ПРОБЛЕМА 2', еще более тонкие. В частности, предположим, что other_routine () делает рекурсивный вызов manage__table (). Переменная table снова может быть изменена совершенно незаметно! После возвращения из other_routine;C) значение cur может снова стать недействительным. Можно подумать (что мы вначале и сделали), что единственным решением является знать это и добавить после вызова функции переназначение cur с соответствующим комментарием. Однако, Брайн Керниган (Brian Kernighan) любезно нас поправил. Если мы используем' индексирование, проблема поддержки указателя даже не возникает: table = (struct table *) malloc(count * siz.eof (struct table)); /* заполнить таблицу */ table[i].i = j; /* Обновить член i-го элемента */ if (/* некоторое условие */) { /* нужно увеличить таблицу */ count += count/2; р = (struct table *) realloc(table, count * sizeof(struct table)); table = p; . . } table[i] .1 = j; /*¦ПРОБЛЕМА 1 устраняется */ other_routine(); /* Рекурсивный вызов, модифицирует таблицу */ table[i].j = k; /* ПРОБЛЕМА 2 также устраняется */ Использование индексирования не решает проблему, если вы используете глобальную копию первоначального указателя на выделенные данные; в этом случае, вам все равно нужно побеспокоиться об обновлении своих глобальных структур после вызова realloc (). I ЗАМЕЧАНИЕ, Как и в случае с mall ос (), когда вы увеличиваете размер памяти, Iвновь выделенная после realloc () память не инициализируется нулями. Вы сами \при необходимости должны очистить память с помощью memsetO, поскольку I realloc () лишь выделяет новую память и больше ничего не делает. 3.2.1.5. Выделение с инициализацией нулями: calloc () Функция calloc () является простой оболочкой вокругmalloc (). Главным ее преимуществом является то, что она обнуляет динамически выделенную память. Она также вычисляет за вас размер памяти, принимая в качестве параметра число элементов и размер каждого элемента: coordinates = (struct coord *) calloc(count, sizeof(struct coord)); По крайней мере идейно, код calloc () довольно простой. Вот одна из возможных реализаций:
76 Глава 3. Управление памятью на уровне пользователя void *calloc(size_t nmemb, size_t size) { void *p; size_t total; total = nmemb * size; /* Вычислить размер */ p = malloc(total); /* Выделить память */ if (p != NULL) /* Если это сработало - */ memset(p, ' \0'-, total); /* Заполнить ее нулями */ return p; /* Возвращаемое значение NULL или указатель */ } Многие опытные программисты предпочитают использовать calloc (), поскольку в этом случае никогда не возникает вопросов по поводу вновь выделенной памяти. Если вы знаете, что вам понадобится инициализированная нулями память, следует также использовать calloc (), поскольку возможно, что память, возвращенная malloc (), уже заполнена нулями. Хотя вы, программист, не можете этого знать, calloc () может это знать и избежать лишнего вызова memset (). 3.2.1.6. Подведение итогов из GNU Coding Standards Чтобы подвести итоги, процитируем, что говорит об использовании процедур выделения памяти GNU Coding Standards: Проверяйте каждый вызов malloc или realloc на предмет возвращенного нуля. Проверяйте realloc даже в том случае, если вы уменьшаете размер блока; в системе, которая округляет размеры блока до степени двойки, realloc может получить другой блок, если вы запрашиваете меньше памяти. В Unix rea,lloc может разрушить блок памяти, если она возвращает ноль. GNU realloc не содержит подобной ошибки: если она завершается неудачей, исходный блок остается без изменений. Считайте, что ошибка устранена. Если вы хотите запустить свою программу на Unix и хотите избежать потерь в этом случае, вы можете использовать GNU malloc. Вы должны считать, что free изменяет содержимое освобожденного блока. Все, что вы хотите получить из блока, вы должны получать до вызова free. В этих трех коротких абзацах Ричард Столмен (Richard Stallman) выразил суть важных принципов управления динамической памятью с помощью malloc (). Именно использование динамической памяти и принцип «никаких произвольных ограничений» делают программы GNU такими устойчивыми и более работоспособными по сравнению с их Unix-двойниками. Мы хотим подчеркнуть, что стандарт С требует, чтобы realloc () не разрушал оригинальный блок памяти, если она возвращает NULL.
3.2. Выделение памяти 77 3.2.1.7. Использование персональных программ распределения Набор функций с mallocO является набором общего назначения по выделению памяти. Он должен быть способен обработать запросы на произвольно большие или маленькие размеры памяти и осуществлять все необходимые учетные действия при освобождении различных участков выделенной памяти. Если ваша программа выделяет значительную динамическую память, вы можете обнаружить, что она тратит большую часть своего времени в функциях malloc (). Вы можете написать персональную программу распределения - набор функций или макросов, которые выделяют большие участки памяти с помощью malloc (), а затем дробят их на маленькие кусочки по одному за раз. Эта методика особенно полезна, если вы выделяете множество отдельных экземпляров одной и той же сравнительно небольшой структуры. Например, GNU awk (gawk) использует эту методику. Выдержка из файла awk.h в дистрибутиве gawk (слегка отредактировано, чтобы уместилось на странице): #define getnode(n) if (nextfree) n = nextfree, \ nextfree = nextfree->nextp; else n = more_nodes() #define freenode(n) ((n)->flags = 0, (n)->exec_count = 0,\ (n)->nextp = nextfree, nextfree = (n)) Переменная nextfree указывает на связанный список структур NODE. Макрос getnode() убирает из списка первую структуру, если она там есть. В противном случае она вызывает more__nodes (), чтобы выделить новый список свободных структур NODE. Макрос f reenode () освобождает структуру NODE, помещая его в начало списка. {ЗАМЕЧАНИЕ, Первоначально при написании своего прилоэ/сения делайте это про- Хстым способом: непосредственно используйте malloc О и free(). Написание I собственного распределителя вы должны рассмотреть лишь в том и только в том {случае, если профилирование вашей программы покажет, что она значительную I часть времени проводит в функциях выделения памяти. 3.2.1.8. Пример: чтение строк произвольной длины Поскольку это, в конце концов, Программирование на Linux в примерах, настало время для . примера из реальной жизни. Следующий код является функцией readline () из GNU Make 3.80 (ftp: //ftp.gnu.org/gnu/make/make-3 .80 .tar.gz). Ее можно найти в файле read. с. Следуя принципу «никаких произвольных ограничений», строки в Makefile могут быть любой длины. Поэтому главной задачей этой процедуры является чтение строк произвольной длины и гарантирование того, что они помещаются в используемый буфер. Вторичной задачей является распоряжение продлением строк. Как и в С, строки, заканчивающиеся обратным слешем, логически продолжаются со следующей строки. Используется стратегия поддержания буфера. В нем хранится столько строк, сколько поме-
78 Глава 3. Управление памятью на уровне пользователя щается в буфер, причем указатели отслеживают начало буфера, текущую строку и следующую строку Вот структура: struct ebuffer { char ^buffer; /* Начало текущей строки в буфере. */ char *bufnext; /* Начало следующей строки в буфере. */ char *bufstart; /* Начало всего буфера. */ unsigned int size; /* Размер буфера для malloc. */¦ FILE *fp; /* Файл или NULL, если это' внутренний буфер. */ struct floe floe; /* Информация о файле в fp (если он есть). */ }; Поле size отслеживает размер всего буфера, a f p является указателем типа FILE для файла ввода. Структура floe не представляет интереса при изучении процедуры. Функция возвращает число строк в буфере. (Номера строк здесь даны относительно начала функции, а не исходного файла.) 1 static long 2 readline (ebuf) /* static long deadline(struct ebuffer *ebuf) */ 3 struct ebuffer *ebuf; 4 { 5 char *p; 6 char *end; 7 char *start; 8 long nlines = 0; 9 10 /* Использование строковых буферов и буферов потоков достаточно 11 различается, чтобы использовать разные функции. */ 12 13 if (!ebuf->fp) 14 return readstring (ebuf); 15 16 /* При чтении из файла для. каждой новой строки мы всегда 17 начинаем с начала буфера. */ 18 19 р = start = ebuf->bufstart; 2 0 end = p + ebuf->size; 21 *p = '\0«; Для начала заметим, что GNU Make написан на С K&R для максимальной переносимости. В исходной части объявляются переменные, и если ввод осуществляется из строки (как в случае расширения макроса), код вызывает другую функцию, reads tring () (строки 13 и 14). Строка '!ebuf->fp' (строка 13) является более короткой (и менее понятной, по нашему мнению) проверкой на пустой указатель; это то же самое, что и 'ebuf ->fp==NULL\
3.2. Выделение памяти 79 Строки 19-21 инициализируют указатели и вводят байт NUL, который является символом завершения строки С в конце буфера. Затем функция входит в цикл (строки 23-95), который продолжается до завершения всего ввода. 23 while (fgets (p, end - р, ebuf->fp) != 0) 24 { 2 5 char *p2; 26 unsigned long len; 27 int backslash; 28 29 len = strlen (p); 30 if (len == 0) 31 '{ 32 /* Это случается лишь тогда, когда первый символ строки ' \0'. 33 Это довольно безнадежный случай, но (верите или нет) ляп Афины 34 бьет снова! (xmkmf помещает NUL в свои makefile.) 35 Здесь на самом деле нечего делать; мы создаем новую строку, чтобы 36 следующая строка не была частью данной строки. */ 37 error (&ebuf->floc, 38 _("warning: NUL character seen; rest of line ignored")); 39 p[0] = ,\n'; 40 len = 1; 41 } Функция fgets () (строка 23) принимает указатель на буфер, количество байтов для прочтения и переменную FILE* для файла, из которого осуществляется чтение. Она читает на один байт меньше указанного, чтобы можно было завершить буфер символом '\0\ Эта функция подходит, поскольку она позволяет избежать переполнения буфера. Она прекращает чтение, когда встречается с символами конца строки или конца файла; если это символ новой строки, он помещается в буфер. Функция возвращает NULL при неудаче или значение указателя первого аргумента при успешном завершении. В этом случае аргументами являются указатель на свободную область буфера, размер оставшейся части буфера и указатель FILE для чтения. Комментарии в строках 32-36 очевидны; если встречается нулевой байт, программа выводит сообщение об ошибке и представляет вывод как пустую строку. После компенсирования нулевого байта (строки 30-41) код продолжает работу. 43 /* Обойти только что прочитанный текст. */ 44 р += len; 45 46 /* Если последний символ - не конец строки, она не поместилась 47 ' целиком в буфер. Увеличить буфер и попытаться снова. */ 48 if (p[-l] != 'Mi' ) 49 goto more_buffer; 50
80 Глава 3. Управление памятью на уровне пользователя 51 /* Мы получили новую строку, увеличить число строк. */ 52 ++nlines; Строки 43-52 увеличивают указатель на участок буфера за только что прочитанными данными. Затем код проверяет, является ли последний прочитанный символ символом конца строки. Конструкция р [ -1 ] (строка 48) проверяет символ перед р, также как р [ 0 ] является текущим символом, а р[1] - следующим. Сначала это кажется странным, но если вы переведете это на язык математики указателей, * (р-1), это приобретет больший смысл, а индексированная форма, возможно, проще для чтения. Если последний символ не был символом конца строки, это означает, что нам не хватило места, и код выходит (с помощью goto) для увеличения размера буфера (строка 49). В противном случае увеличивается число строк. 54 #if !defined(WINDOWS32) && !defined( MSDOS ) 55 /* Проверить, что строка завершилась CRLF; если так, 56 игнорировать CR. */ 57 if ((р - start) > 1 && р[-2] == ' \г') 58 { 59 —р; 60 р[-1] = '\п'; 61 } 62 #endif Строки 54-62 обрабатывают вводимые строки, следующие соглашению Microsoft по завершению строк комбинацией символов возврата каретки и перевода строки (CR-LF), а не просто символом перевода строки (новой строки), который является соглашением Linux/ Unix. Обратите внимание, что #if def исключает этот код на платформе Microsoft; очевидно, библиотека <stdio. h> на этих системах автоматически осуществляет это преобразование. Это верно также для других не-Unix систем, поддерживающих стандартный С. 64 backslash = 0; 65 for (p2 = р - 2; р2 >= start; --p2) 66 { 67 if (*р2 != '\\') 68 break; 69 backslash = !backslash; 70 } 71 72 if (Ibackslash) 73 { 74 p[-l] = '\0'; 7 5 break; 76 } 77 7 8 /* Это была комбинация обратный слеш/новая строка. Если есть 7 9 место, прочесть еще одну строку. */
3.2. Выделение памяти 81 80 if (end - р >= 80) 81 continue; 82 83 /* В конце буфера нужно больше места, поэтому выделить еще. 84 Позаботиться о сохранении текущего смещения в р. */ 85 more_jDuf fer: 86 { 87 unsigned long off = p - start; 88 ebuf->size *= 2; 89 start=ebuf->buffer=ebuf->bufstart=(char *)xrealloc (start, 90 ebuf->size) ; 91 p = start + off; 92 end = start + ebuf->size; 93 *p = '\0'; 94 ¦ } 95 } До сих пор мы имели дело с механизмом получения в буфер по крайней мере одной полной строки. Следующий участок обрабатывает случай строки с продолжением. Хотя он должен гарантировать, что конечный символ обратного слеша не является частью нескольких обратных слешей в конце строки. Код проверяет, является ли общее число таких символов четным или нечетным путем простого переключения переменной backslash из 0 в 1 и обратно. (Строки 64-70.) Если число четное, условие ' ibackshlash' (строка 72) будет истинным. В этом случае конечный символ конца строки замещается байтом NUL, и код выходит из цикла. С другой стороны, если число нечетно, строка содержит четное число пар обратных слешей (представляющих символы \\, как в С), и конечную комбинацию символов обратного слеша и конца строки6. В этом случае, если в буфере остались по крайней мере 80 свободных байтов, программа продолжает чтение в цикле следующей строки (строки 78-81). (Использование магического числа 80 не очень здорово; было бы лучше определить и использовать макроподстановку.) По достижении строки 83 программе нужно больше места в буфере. Именно здесь вступает в игру динамическое управление памятью. Обратите внимание на комментарий относительно сохранения значения р (строки 83-84); мы обсуждали это ранее в терминах повторной инициализации указателей для динамической памяти. Значение end также устанавливается повторно. Строка 89 изменяет размер памяти. Обратите внимание, что здесь вызывается функция xrealloc (). Многие программы GNU используют вместо mallocO и realloc() функции-оболочки, которые автома- Этот код несет с собой аромат практического опыта: не удивительно было узнать, что более, ранние версии просто проверяли наличие обратного слеша перед символом конца строки, пока кто-то не пожаловался, что он не работает, когда в конце строки есть несколько обратных слешей. - Примеч. автора.
82 Глава 3. Управление памятью на уровне пользователя тически выводят сообщение об ошибке и завершают программу, когда стандартные процедуры возвращают NULL. Такая функция-оболочка может выглядеть таким образом: extern const char *myname; /* установлено в mairi() */ void *xrealloc(void *ptr, size_t amount) { void *p = realloc(ptr, amount); if (p == NULL) { fprintf(stderr, "%s: out of memory!\n", myname); exitA); . } return p; } Таким образом, если функция xrealloc () возвращается, она гарантированно возвращает действительный указатель. (Эта стратегия соответствует принципу «проверки каждого вызова на ошибки», избегая в то же время беспорядка в коде, который происходит при таких проверках с непосредственным использованием стандартных процедур.) Вдобавок, это позволяет эффективно использовать конструкцию 4ptr = xrealloc (ptr, new__size)\ против которой мы предостерегали ранее. Обратите внимание, что не всегда подходит использование такой оболочки. Если вы сами хотите обработать ошибки, не следует использовать оболочку. С другой стороны, если нехватка памяти всегда является фатальной ошибкой, такая оболочка вполне удобна. 97 if (ferror (ebuf->fp)) 98 pfatal_with_name (ebuf->floc.filenm); 99 100 /* Если обнаружено несколько строк, возвратить их число. 101 Если не несколько, но _что-то_ нашли, значит, прочитана 102 последняя строка файла без завершающего символа конца 103 строки; вернуть 1. Если ничего не прочитано, это EOF; 104 возвратить -1. */ 105 return nlines ? nlines : р'== ebuf->bufstart ? -1 : 1; 106 } В заключение, функция readline () проверяет ошибки ввода/вывода, а затем возвращает описательное значение. Функция pf atal_with__name () (строка 98) не возвращается7. 3.2.1.9. Только GUBC: чтение целых строк: get line () иде tdel im () Теперь, когда вы увидели, как читать строки произвольной длины, вы можете сделать вздох облегчения, что вам не нужно самим писать такую функцию. GLIBC предоставляет вам для этого две функции: #define _GNU_SOURCE 1 /* GLIBC */ #include <stdio.h> 7 Эта функция завершает выполнение программы. - Примеч. науч. ред.
3.2. Выделение памяти 83 #include <sys/types.h> /* для ssize_t */ ssize__t getline(char **lineptr, size_t *n, FILE *stream);' ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream); Определение константы _GNU_SOURCE вводит объявления функций getlineO и getdelim(). В противном случае они неявно объявлены как возвращающие int. Для объявления возвращаемого типа ssize__t нужен файл <sys/types .h>. (ssize_t является «знаковым size_t». Он предназначен для такого же использования, что и size„t, но в местах, где может понадобиться использование также и отрицательных значений.) Обе функции управляют для вас динамической памятью, гарантируя, что буфер, содержащий входную строку, достаточно большой для размещения всей строки. Их отличие друг от друга в том, что getlineO читает до символа конца строки, a getdelimO использует в качестве разделителя символ, предоставленный пользователем. Общие аргументы следующие: char **lineptr Указатель на char* указатель для адреса динамически выделенного буфера. Чтобы get line () сделала всю работу, он должен быть инициализирован NULL. В противном случае, он должен указывать на область памяти, выделенную с помощью malloc (). Size_t *n Указатель на размер буфера. Если вы выделяете свой собственный буфер, *п должно содержать размер буфера. Обе функции рбновляют *п новым значением размера буфера, если они его изменяют. FILE *stream Место, откуда следует получать входные символы. По достижении конца файла или при ошибке функция возвращает -1. Строки содержат завершающий символ конца строки или разделитель (если он есть), а также завершающий нулевой байт. Использование getlineO просто, как показано B-.chQ'3-getline. с: /* ch03-getline.с демонстрация getlineO. */ #define _GMJ__SOURCE 1 #include <stdio.h> #include <sys/types,h> /* main - прочесть строку и отобразить ее, пока не достигнут EOF */ int main(void) { • char *line = NULL; size_t size = 0; ssize_t ret; while ((ret = getline(& line, & size, stdin)) != -1) printf("(%lu) %s", size, line); return 0; }
84 Глава 3. Управление памятью на уровне пользователя Вот эта функция в действии, показывающая размер буфера. Третья входная и выходная строки намеренно длинные, чтобы заставить get line () увеличить размер буфера: $ ch03-getline /* Запустить программу */ this is a line A20) this is a line And another line. A2 0) And another line. ' A 11111111111111111 oooooooooooooooooooooooooooooooonnnnnnnnnnnnnnnnnn ngggggggggggg llliiiiiiiiiiiiiiiiiiinnnnnnnnnnnnnnnnnnnneeeeeeeeee B40) A llllllllllllllllloooooooooooooooooooooooooooooooonnnnnnnnnn nnnnnngnnnggggggggggg llliiiiiiiiiiiiiiiiiiinnnnnnnnnnnnnnnnnnnneeeeeeeeee 3.2.2. Копирование строк: strdup () Одной чрезвычайно типичной операцией является выделение памяти для копирования строки. Это настолько типично, что многие программисты предусматривают для нее простую функцию вместо использования внутритекстового кодирования, и часто эта функция называется strdup () : #include <string.h> /* strdup выделить память с ша11ос().и скопировать строку */ char *strdup(const char *str) { size_t len; char *copy; len = strlen(str) + 1; /* включить место для завершающего '\0' */ сору = malloc(len); if (copy != NULL) strcpy(copy, str) ; return copy; /* при ошибке возвращает NULL */ } - С появлением стандарта POSIX 2001 программисты по всему миру могут вздохнуть свободнее: эта функция является теперь частью POSIX в виде расширения XSI: #include <string.h> '/* XSI */ char *strdup(const char *str); /* Копировать str */ Возвращаемое значение равно NULL, если была ошибка, или указатель на динамически выделенную память с копией str. Возвращенное значение должно быть освобождено с помощью free (), когда больше не требуется.
3.2. Выделение памяти 85 3.2.3. Системные вызовы: brk () и sbrk () Четыре функции, которые мы рассмотрели (malloc (), callocO, reallocO и free ()) являются стандартными, переносимыми функциями для управления динамической памятью. На Unix-системах стандартные функции реализованы поверх двух дополнительных, очень примитивных процедур, которые непосредственно изменяют размер адресного пространства процесса. Мы представляем их здесь, чтобы помочь вам понять, как работают GNU/Linux и Unix (снова «под капотом»); крайне маловероятно, что вам когда-нибудь понадобится использовать эти функции в обычных программах. Они определены следующим образом: #include <unistd.h> /* Обычный */ #include <malloc.h:> /* Необходим для систем GLIBC 2 */ int brk(void *end_data_segment); void *sbrk(ptrdiff_t increment); Системный вызов brk () действительно изменяет адресное пространство процесса. Адрес является указателем, представляющим окончание сегмента данных (на самом деле, области кучи, как было показано ранее на рис. 3.1). Ее аргумент является абсолютным логическим адресом, представляющим новое окончание адресного пространства. В случае успеха функция возвращает 0, а в случае неуспеха (-1). Функцию sbrk () использовать проще; ее аргумент является числом байтов, на которое нужно увеличить адресное пространство. Вызвав ее с приращением 0, можно определить," где в настоящее время заканчивается адресное пространство. Таким образом, чтобы увеличить адресное пространство на 32 байта, используется код следующего вида: char *p = (char *) sbrk(O);. /* получить текущий конец адресного пространства */ if (brk(p +32) < 0) { /* обработать ошибку */ } /* в противном случае, изменение сработало.*/ Практически, вам не нужно непосредственно использовать brk (). Вместо этого используется исключительно sbrk() для увеличения (или даже сокращения) адресного пространства. (Вскоре мы покажем, как это делать, в разделе 3.2.5. «Исследование адресного пространства».) Еще более практично вообще никогда не использовать эти процедуры. Программа, которая их использует, не может затем использовать также и malloc (), и это создает большую проблему, поскольку многие элементы стандартной библиотеки полагаются на использование malloc (). Поэтому использование brk () или sbrk () может приводить к трудно обнаруживаемым крушениям программы. Но знать о низкоуровневых механизмах стоит, и конечно же, набор функций malloc () реализован с помощью sbrk () и brk ().
.86 Глава 3. Управление памятью на уровне пользователя 3.2.4. Вызовы ленивых программистов: alloca () «Опасность, Билл Робинсон! Опасность!» - Робот - Есть еще одна дополнительная функция выделения памяти, о которой вам нужно знать. Мы обсуждаем ее лишь для того, чтобы вы поняли ее, когда увидите, но не следует использовать ее в новых программах! Эта функция называется alloca (); она объявлена следующим образом: /* Заголовок в GNU/Linux, возможно, не на всех Unix-системах */ #include <alloca.h> /* Обычный */ void *alloca(size_t size); Функция alloca () выделяет size байтов из стека. Хорошо, что выделенная память исчезает после возвращения из функции. Нет необходимости явным образом освобождать память, поскольку это осуществляется автоматически, как в случае с локальными переменными. На первый взгляд, alloca () выглядит чем-то типа панацеи для программистов; можно выделять память, о которой можно вовсе не беспокоиться. Подобно Темной Стороне Силы, это, конечно, привлекает. И подобным же образом этого нужно избегать по следующим причинам: функция не является стандартной; она не включена ни в какой стандарт, ни в ISO, ни в С или POSIX. Функция не переносима. Хотя она существует на многих системах Unix и GNU/Linux, она не существует на не-Unix системах. Это проблема, поскольку код часто должен быть многоплатформенным, выходя за пределы просто Linux и Unix. • На некоторых системах alloca () невозможно даже реализовать. Весь мир не является ни процессором Intel x86, ни GCC. • Цитируя справку8 (добавлено выделение): «Функция alloca зависит от машины и от компилятора. На многих системах ее реализация ошибочна. Ее использование не рекомендуется». • Снова цитируя справку: «На многих системах alloca не может быть использована внутри списка аргументов вызова функции, поскольку резервируемая в стеке при помощи alloca память оказалась бы в середине стека в пространстве для аргументов функции». • Она потворствует неряшливому программированию. Тщательная и корректная работа с памятью не сложна; вам просто нужно подумать ю том, что вы делаете, и планировать заранее. GCC обычно использует встроенную версию функции, которая действует с использованием внутритекстового (inline) кода. В результате есть другие последствия alloca (). Снова цитируя справку: факт, что код является внутритекстовым (inline), означает, что невозможно получить адрес этой функции или изменить ее поведение путем компоновки с другой библиотекой. alloca C ) - Примеч. науч. ред.
3.2. Выделение памяти 87 Внутритекстовый код часто состоит из одной инструкции, подгоняющей указатель стека, и не проверяет переполнение стека. Поэтому нет возврата NULL при ошибке. Справочная страница не углубляется в описание проблемы со встроенной alloca () GCC. Если есть переполнение стека, возвращаемое значение является мусором. И у вас нет способа сообщить об этом! Это упущение делает невозможным использование GCC alloca () в устойчивом коде. Все это должно убедить вас избегать alloca () в любом новом коде, который вы пишете. В любом случае, если приходится писать переносимый код с использованием malloc () и free (), нет причины в использовании также и alloca (). 3.2.5. Исследование адресного пространства Следующая программа, ch03-memaddr .с, подводит итог всему, что мы узнали об адресном пространстве. Она делает множество вещей, которые не следует делать на практике, таких, как вызовы alloca () или непосредственные вызовы brk () и sbrk (): 1 /* 2 * ch03-memaddr.с Показать адреса секций кода, данных и стека, 3 * а также BSS и динамической памяти. 4 */ 5 6 #include <stdio.h> 7 #include <malloc.h> /* для определения ptrdiff_t в GLIBC */ 8 #include <unistd.h> 9 #include <alloca.h> /* лишь для демонстрации */ 10 11 extern void afunc(void); /* функция, показывающая рост стека */ 12 13 int bss_var; /* автоматически инициализируется в 0, должна быть в BSS */ 14 int data_var =42; /* инициализируется в не 0, должна быть 15 в сегменте данных */ 16 int 17 main(int argc, char **argv) /* аргументы не используются */ 18 { 19 char *pf *b, *nb; 20 21 printf("Text Locations:\n"); 22 printf("\tAddress of main: %p\n", main); 23 printf("\tAddress of afunc: %p\n", afunc); 24 25 printf("Stack Locations:\n"); 2 6 afunc(); 27 28 p = (char *) allocaC2);
88 Глава 3. Управление памятью на уровне пользователя 29 if (p != NULL) { 30 printf("\tStart of allocaO'ed array: %p\nM, p) ; 31 printf ("\tEnd of allocaO'ed array: %p\n'\ p + 31); 32 } 33 34 printf("Data Locations:\n"); 35 printf (" \tAddress of data_var: %p\n", & data__var) ; 36 37 printf("BSS Locations:\n"); 38 printf("\tAddress of bss_var: %p\n", & bss_var); 39 40 b = sbrk((ptrdiff_t) 32); /* увеличить адресное пространство */ 41 nb = sbrk((ptrdiff_t) 0); 42 printf("Heap Locations:\n"); 43 printf("\tlnitial end of heap: "%p\n", b); 44 printf("\tNew end of heap: %p\n", nb) ; 45 46 b = sbrk((ptrdiff_t) -16); /* сократить его */ 47 nb = sbrk((ptrdiff_t) 0); 48 printf("\tFinal end of heap: %p\n'\ nb) ; 49 } 50 51 void 52 afunc(void) 53 { 54 static int level =0; /* уровень рекурсии */ 55 auto int stack_var; /* автоматическая переменная в стеке */ 56 57 if (++level ==3) /* избежать бесконечной рекурсии */ 58 return; 59 60 printf("\tStack level %d: address of stack_var: %p\n", 61 level, & stack_var) ; 62 afunc(); /* рекурсивный вызов */ 63 } Эта программа распечатывает местонахождение двух функций main () и af unc () (строки 22-23). Затем она показывает, как стек растет вниз, позволяя af unc () (строки 51-63) распечатать адреса последовательных экземпляров ее локальной переменной stack__var. (stack__var намеренно объявлена как auto, чтобы подчеркнуть, что она находится в стеке.) Затем она показывает расположение памяти, выделенной с помощью alloca() (строки 28-32). В заключение она печатает местоположение переменных данных и BSS (строки 34-38), а затем памяти, выделенной непосредственно через sbrk () (строки 40-48). Вот результаты запуска программы на системе Intel GNU/Linux:
3.3. Резюме 89 $ ch03-memaddr Text Locations: Address of main: 0x804838c Address of afunc: 0x80484a8 Stack Locations: Stack level 1: address of stack_var: Oxbffff'864 Stack level 2: address of stack_var: 0xbffff844 /* Стек растет вниз */ Start of .allocaO'ed array: 0xbffff860 End of allocaO'ed array: 0xbffff87f /* Адреса находятся в стеке */ Data Locations: Address of data_var: 0x80496b8 BSS Locations: Address of bss.var: 0x80497c4 /* BSS выше инициализированных данных */ Heap Locations: Initial end of heap: 0x80497c8 /* Куча непосредственно над BSS */ New end of heap: 0x80497e8 /* И растет вверх */ Final end of heap: 0x80497d8 /* Адресные пространства можно сокращать */ 3.3. Резюме У каждой программы Linux и (Unix) есть различные области памяти. Они хранятся в разных частях файла исполняемой программы на диске. Некоторые из секций загружаются при запуске программы в одну и ту же область памяти. Все запущенные экземпляры одной и той же программы разделяют исполняемый код (сегмент текста). Программа size показывает размеры различных областей переместимых объектных файлов и полностью скомпонованных исполняемых файлов. В адресном пространстве запущенной программы могут быть дыры, а размер адресного пространства может изменяться при выделении и освобождении памяти. На современных системах адрес 0 не является частью адресного пространства, поэтому не пытайтесь разыменовывать указатели NULL. • На уровне языка С память выделяется с помощью одной из функций mallocO, callocO или reallocO. Память освобождается с помощью free(). (Хотя с помощью realloc () можно делать все, использование ее таким образом не рекомендуется.) Освобожденная память обычно не удаляется из адресного пространства; вместо этого она используется повторно при последующих выделениях. Необходимо предпринять чрезвычайные меры осторожности в следующих случаях:
90 Глава 3. Управление памятью на уровне пользователя • освобождать лишь память, выделенную с помощью соответствующих процедур, • освобождать память один и только один раз, • освобождать неиспользуемую память и • не допускать «утечки» динамически выделяемой памяти. POSIX предоставляет для удобства функцию strdupO, a GLIBC предоставляет функции get line () и getdelim() для чтения строк произвольной длины. Функции интерфейса низкоуровневых системных вызовов brk () и sbrk () предоставляют непосредственный, но примитивный доступ к выделению и освобождению памяти. Если вы не создаете свой собственный распределитель памяти, следует избегать их. Существует функция alloca () для выделения памяти в стеке, но ее использование не рекомендуется. Подобно умению распознавать ядовитый плющ, про нее нужно знать лишь для того, чтобы избегать ее. Упражнения 1. Начав со структуры - struct line { size_t buflen; char *buf; FILE *fp; }; '- напишите свою собственную функцию readline (), которая будет читать строки любой длины. Не беспокойтесь о строках, продолженных с помощью обратного слеша. Вместо использования f getc () для чтения строк используйте getc {) для чтения одного символа за раз. 2. Сохраняет ли ваша функция завершающий символ конца строки? Объясните, почему. 3. Как ваша функция обрабатывает строки, оканчивающиеся CR-LF? 4. Как вы инициализируете структуру? В отдельной процедуре? С помощью документированных условий для определенных значений в структуре? 5. Как вы обозначаете конец файла? Как вы указываете, что возникла ошибка ввода/вывода? Должна ли ваша функция сообщать об ошибках? Объясните, почему. 6. Напишите программу, которая использует вашу функцию для ее тестирования, а также другую программу, создающую входные данные для первой программы. Протестируйте функцию. 7. Перепишите вашу функцию с использованием f gets () и протестируйте ее. Является ли новый код более сложным или менее сложным? Какова его производительность по сравнению с версией getc () ? 8. Изучите страницу справки V7 для ewrfC)(/usr/man/man3/end.3 в дистрибутиве V7). Пролила ли она свет на то, как может работать 'sbrk @)'? 9. Усовершенствуйте ch03 -memaddr. с так, чтобы она печатала расположение аргументов и переменных окружения. В какой области адресного пространства они находятся?
Глава 4 Файлы и файловый ввод/вывод В этой главе: • 4.1. Введение в модель ввода/вывода Linux/Unix 91 • 4.2. Представление базовой структуры программы ,•. .92 • 4.3. Определение ошибок 93 • 4.4. Ввод и вывод 99 • 4.5. Произвольный доступ: перемещения внутри файла / 108 • 4.6. Создание файлов 113 • 4.7. Форсирование записи данных на диск 119 • 4.8. Установка длины файла 120 • 4.9. Резюме : 120 • Упражнения 121 Данная глава описывает базовые файловые операции: открытие и создание файлов, чтение и запись в них, перемещение в них и их закрытие. По ходу дела она представляет стандартные механизмы для обнаружения ошибок и сообщений о них. Глава заканчивается описанием того, как установить длину файла и принудительно сбросить данные файла и вспомогательные данные на диск. 4.1. Введение в модель ввода/вывода Linux/Unix Модель API Linux/Unix для ввода/вывода проста. Ее можно суммировать четырьмя словами: открыть, прочитать, записать, закрыть. Фактически, это имена системных вызовов: open (), read (), write (), close (). Вот их объявления: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> /* для .mode_t */ #include <fcntl.h> /* для flags для open() */ #include <unistd.h> /* для ssize_t */¦ int open(const char ^pathname, int flags, mode__t mode); ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); int close(int fd);
92 Глава 4. Файлы и файловый ввод/вывод В следующем и дальнейших разделах мы проиллюстрируем модель, написав очень простую версию cat. Она так проста, что даже не имеет опций; все, что она делает, - объединяет содержимое двух именованных файлов и выводит его в стандартный вывод. Она выводит минимум сообщений об ошибках. Написав, мы сравним ее с V7 cat. Мы представим программу сверху вниз, начав с командной строки. В последующих разделах мы представим обработку ошибок, а затем перейдем к сущностным задачам, показав, каким образом осуществляется реальный файловый ввод/вывод. 4.2. Представление базовой структуры программы Наша версия cat следует структуре, которая обычно является полезной. Первая часть начинается с комментариев, заголовочных файлов, объявлений и функции main (): 1 /* 2 * ch04-cat.c Демонстрация open () , read(), write(), close(), 3 * errno и strerrorO . 4 */ 5 6 #include <stdio.h> /* для fprintfO, stderr, BUFSIZ */ 7 #include <errno.h> /* объявление errno */ 8 #include <fcntl.h> /* для flags для open()¦*/ 9 #include <string.h> /* объявление strerrorO */ 10 #include <unistd.h> /* для ssize__t */. 11 #include <sys/types.h> 12 #include <sys/stat.h> /¦* для mode_t */ 13 14 char *myname; 15 int process(char *file); 16 17 /* main перечислить аргументы файла ¦*/ 18 19 int 20 main(int argc, char **argv) 21 { 22 int i; 23 int errs = 0; 24 2 5 myname = argv[0] ; 26 27 if (argc == 1) 28 errs = process("-") ;
4.3. Определение ошибок 93 29 else 30 for (i =1; i < argc; i++) 31 errs += process(argv[i]); 32 33 return (errs != 0); 34 } ... продолжение далее в главе ... Переменная myname (строка 14) используется далее для сообщений об ошибках; main () первым делом устанавливает в ней имя программы (argv[0]). Затем main() в цикле перечисляет аргументы. Для каждого аргумента она вызывает функцию process (). Когда в качестве имени файла дано - (простая черточка, или знак минус), cat Unix вместо попытки открыть файл с именем '-' читает стандартный ввод. Вдобавок, cat читает стандартный ввод, когда нет аргументов. ch04-cat реализует оба этих поведения. Условие 'argc == 1' (строка 27) истинно, когда нет аргументов имени файла; в этом случае main () передает «-» функции process (). В противном случае, main() перечисляет аргументы, рассматривая их как файлы, которые необходимо обработать. Если один из них окажется «-», программа обрабатывает стандартный ввод. Если process () возвращает ненулевое значение, это означает, что случилась какая- то ошибка. Ошибки подсчитываются в переменной errs (строки 28 и 31). Когда main () завершается, она возвращает 0, если не было ошибок, и 1, если были (строка 33). Это довольно стандартное соглашение, значение которого более подробно обсуждается в разделе 9.1.5.1 «Определение статуса завершения процесса». Структура, представленная в main (), довольно общая: process () может делать с файлом все, что мы захотим. Например (игнорируя особый случай «-»), process () также легко могла бы удалять файлы вместо их объединения! Прежде чем рассмотреть функцию process (), нам нужно описать, как представлены ошибки системных вызовов и как осуществляется ввод/вывод. Сама функция process () представлена в разделе 4.4.3 «Чтение и запись». 4.3. Определение ошибок «Если неприятность может произойти, она случается» - Закон Мерфи - «Будь готов» - Бойскауты - Ошибки могут возникнуть в любое время. Диски могут заполниться, пользователи могут ввести неверные данные, сетевой сервер, с которого осуществляется чтение, может отказать, сеть может выйти из строя и т. д. Важно всегда проверять успешность завершения каждой операции.
94 Глава 4. Файлы и файловый ввод/вывод Основные системные вызовы Linux почти всегда возвращают при ошибке -1 и 0 или положительное значение при успехе. Это дает возможность узнать, была операция успешной или нет: int result; result = some_system_call(paraml, param2); if (result < 0) { v /* ошибка, что-нибудь сделать */ } else /¦* все нормально, продолжить */ Знания того, что произошла ошибка, недостаточно. Нужно знать, какая произошла ошибка. Для этого у каждого процесса есть предопределенная переменная с именем errno. Всякий раз, когда системный вызов завершается ошибкой, errno устанавливается в один из набора предопределенных значений ошибок, errno и предопределенные значения ошибок определены в файле заголовка <errno. h>: #include <errno.h> /* ISO С */ extern int errno; Хотя сама errno может быть макросом, который действует подобно переменной int - она не обязательно является действительной целой переменной. В частности, в многопоточном окружении у каждого потока будет своя индивидуальная версия errno. Несмотря на это, практически для всех системных вызовов и функций в данной книге вы можете рассматривать errno как простую int. 4.3.1. Значения errno Стандарт POSIX 2001 определяет большое число возможных значений для errno. Многие из них относятся к сетям, IPC или другим специальным задачам. Справочная страница для каждого системного вызова описывает возможные значения errno, которые могут иметь место; поэтому вы можете написать код для проверки отдельных ошибок и соответствующим образом обработать их, если это нужно. Возможные значения определены через символические имена. Предусмотренные GLIBC значения перечислены в табл. 4.1. Таблица 4.1. Значения GLIBC для errno Имя Значение B2BIG Слишком длинный список аргументов. EACCESS Доступ запрещен. EADDRINUSE Адрес используется-. EADDRNOTAVAIL Адрес недоступен. EAFNOSUPPORJT Семейство адресов не поддерживается.
4.3. Определение ошибок Таблица 4.1. Значения GLIBC для errno (Продолжение) 95 Имя Значение EAGAIN EALREADY EBADF EBADMSG EBUSY ECANCELED ECHILD ECONNABORTED ECONNFRFUSED ECONNRESET EDEADLK EDESTADDRREQ EDOM EDQUOT EEXIST EFAULT EFBIG EHOSTUNREACH EIDRM . EILSEQ EINPROGRESS EINTR EINVAL ЕЮ EISCONN EISDIR ELOOP Ресурс недоступен, попытайтесь снова (может быть то же самое значение, что EWOULDBLOCK). Соединение уже устанавливается. Ошибочный дескриптор файла. Ошибочное сообщение. Устройство или ресурс заняты. Отмена операции. Нет порожденного процесса. Соединение прервано. Соединение отклонено. Восстановлено исходное состояние соединения. Возможен тупик (deadlock) в запросе ресурса. Требуется адрес назначения. Математический аргумент выходит за область определения функции. Зарезервировано. Файл существует. Ошибочный адрес. Файл слишком большой. Хост недоступен. Идентификатор удален. Ошибочная последовательность байтов. Операция исполняется. Прерванная функция. Недействительный аргумент. Ошибка ввода/вывода. Сокет (уже) соединен. Это каталог. Слишком много уровней символических ссылок.
96 Глава 4. Файлы и файловый ввод/вывод Таблица 4.1. Значения GLIBC для errno (Продолжение) Имя Значение EMFILE EMLINK EMSGSIZE EMULTIHOP ENAMETOOLONG ENETDOWN ENETRESET ENETUNREACH ENFILE ENOBUFS ENODEV ENOENT ENOEXEC ENOLCK ENOLINK ENOMEM ENOMSG ENOPROTOOPT ENOSPC ENOSYS ENOTCONN ENOTDIR ENOTEMPTY ENOTSOCK ENOTSUP ENOTTY ENXIO EOPNOTSUPP Слишком много открытых файлов. Слишком много ссылок. Сообщение слишком длинное. Зарезервировано. Имя файла слишком длинное. Сеть не работает. Соединение прервано сетью. Сеть недоступна. В системе открыто слишком много файлов. Буферное пространство недоступно. Устройство отсутствует. Файл или каталог отсутствуют. Ошибочный формат исполняемого файла. Блокировка недоступна. Зарезервировано. Недостаточно памяти. Сообщение нужного типа отсутствует. Протокол недоступен. Недостаточно памяти в устройстве. Функция не поддерживается. Сокет не соединен. Это не каталог. Каталог не пустой. Это не сокет. Не поддерживается. Неподходящая операция управления вводом/выводом. Нет такого устройства или адреса. Операция сокета не поддерживается.
4.3. Определение ошибок 97 Таблица 4.1. Значения GLIBC для errno (Продолжение) Имя Значение EOVERFLOW EPERM EPIPE EPROTO EPROTONOSUPPORT EPROTOTYPE ERANGE EROFS ESPIPE ESRCH ESTALE ETIMEDOUT ETXTBSY EWOULDBLOCK EXDEV Слишком большое значение для типа данных. Операция не разрешена. Канал (pipe) разрушен. Ошибка протокола. Протокол не поддерживается. Ошибочный тип протокола для сокета. Результат слишком большой. Файловая система только для чтения. Недействительный поиск. Нет такого процесса. Зарезервировано. Тайм-аут соединения истек. Текстовый файл занят. Блокирующая операция (может быть то же значение, что и для EAGAIN). Ссылка через устройство (cross-device link). Многие системы предоставляют также другие значения ошибок, а в более старых системах может не быть всех перечисленных значений ошибок. Полный список следует проверить с помощью справочных страниц introB) и егтоB) для локальной системы. ХЗАМЕЧАНИЕ, errno следует проверять лишь после того, как возникла ошибка, I и до того, как сделаны дальнейшие системные вызовы. Начальное значение этой I переменной 0. Однако, в промежутках между ошибками ничто не изменяет ее {значения, это означает, что успешный системный вызов не восстанавливает зиаче- \ние 0. Конечно, вы можете вручную установить ее в 0 в самом начале или когда \захотите, однако это делается редко. Сначала мы используем errno лишь для сообщений об ошибках. Для этого имеются две полезные функции. Первая - perror (): #include <stdio.h> /* ISO С */ void perror(const char *s); Функция perror () выводит предоставленную программой строку, за которой следует двоеточие, а затем строка, описывающая значение errno: if (some_system_call(parami, param2) < 0) { perror("system call failed"); 4-159
98 Глава 4. Файлы и файловый ввод/вывод return 1; } Мы предпочитаем функцию strerror (), которая принимает параметр со значением ошибки и возвращает указатель на строку с описанием ошибки: #include <string.h> /* ISO С */ char *strerror(int errnum); strerror () предоставляет для сообщений об ошибках максимальную гибкость, поскольку fprintf () дает возможность выводить ошибки любым нужным нам способом, наподобие этого: if (some_system_call(paraml, param2) < 0) { fprintf(stderr, "%s: %d, %d: some_system_call failed: %s\n", argv[0], paraml, param2, strerror(errno)); return 1; } По всей книге вы увидите множество примеров использования обеих функций. 4.3.2. Стиль сообщения об ошибках Для использования в сообщениях об ошибках С предоставляет несколько специальных макросов. Наиболее широкоупотребительными являются FILE и LINE , которые разворачиваются в имя исходного файла и номер текущей строки в этом файле. В С они были доступны с самого начала. С99 определяет дополнительный предопределенный идентификатор, f unc , который представляет имя текущей функции в виде символьной строки. Макросы используются следующим образом: if (some__system_call (paraml, param2) < 0) { fprintf(stderr, "%s: %s (%s %d) : some_system_call(%d, %d) failed: %s\n", argv[0], func , FILE , LINE , paraml, param2, strerror(errno)); return 1; } Здесь сообщение об ошибке включает не только имя программы, но также и имя функции, имя исходного файла и номер строки. Полный список идентификаторов, полезных для диагностики, приведен в табл. 4.2. Использование FILE и LINE , было вполне обычно для ранних дней Unix, когда у большинства людей были исходные коды и они могли находить ошибки и устранять их. По мере того, как системы Unix становились все более коммерческими, использование этих идентификаторов постепенно уменьшалось, поскольку знание положения в исходном коде дает немного пользы, когда имеется лишь двоичный исполняемый файл. Сегодня, хотя системы GNU/Linux поставляются с исходными кодами, указанный исходный код часто не устанавливается по умолчанию. Поэтому использование этих идеи-
4.4. Ввод и вывод 99 тификаторов для сообщений об ошибках не представляет дополнительной ценности. GNU Coding Standards даже не упоминает их. Таблица 4.2. Диагностические идентификаторы С99 Идентификатор Версия С Значение DATE С89 Дата компиляции в виде «Mmm nn yyyy». FILE Оригинальная Имя исходного файла в виде «program, с». LINE Оригинальная Номер строки исходного файла в виде 42. TIME С89 Время компиляции в виде «hh :mm: ss». f unc C99 Имя текущей функции, как если бы было объявлено const char func [] = "name". } ' 4.4. Ввод и вывод . Все операции Linux по вводу/выводу осуществляются посредством дескрипторов файлов. Данный раздел знакомит с дескрипторами файлов, описывает, как их получать и освобождать, и объясняет, как выполнять с их помощью ввод/вывод. 4.4.1. Понятие о дескрипторах файлов Дескриптор файла является целым значением. Действительные дескрипторы файлов начинаются с 0 и растут до некоторого установленного системой предела. Эти целые фактически являются индексами таблицы открытых файлов для каждого процесса. (Таблица поддерживается внутри операционной системы; она недоступна запущенным программам.) В большинстве современных систем размеры таблиц большие. Команда 4ul imi t -n' печатает это значение: $ ulimit -n 1024 Из С максимальное число открытых файлов возвращается функцией getdtablesize () (получить размер таблицы дескрипторов): #include <unistd.h> /* Обычный */ int getdtablesize(void); Следующая небольшая программа выводит результат работы этой функции: /* ch04-maxfds.с Демонстрация getdtablesize(). */ #include <stdio.h> /* для fprintf(), stderr, BUFSIZ */ #include <unistd.h> /* для ssize_t */ int main(int argc, char **argv) { printfC'max f ds : %d\n", getdtablesize ()) ; exit@); }
100 Глава 4. Файлы и файловый ввод/вывод Неудивительно, что после компиляции и запуска эта программа выводит то же значение, что и ulimit: $ ch04-maxfds max fds: 1024 Дескрипторы файлов содержатся в обычных переменных int; для использования с системными вызовами ввода/вывода можно увидеть типичные объявления вида 'int f d\ Для дескрипторов файлов нет предопределенного типа. В обычном случае каждая программа начинает свою работу с тремя уже открытыми для нее дескрипторами файлов. Это стандартный ввод, стандартный вывод и стандартная ошибка, с дескрипторами файлов 0, 1 и 2 соответственно. (Если не было использовано перенаправление, каждый из них связан с клавиатурой и с экраном.) Очевидные символические константы. Оксюморон? При работе с системными вызовами на основе дескрипторов файлов и стандартных ввода, вывода и ошибки целые константы 0, 1 и 2 обычно используются прямо в коде. В подавляющем большинстве случаев использование таких символических констант (manifest constants) является плохой мыслью. Вы никогда не знаете, каково значение некоторой случайной целой константы и имеет ли к ней какое-нибудь отношение константа с тем же значением, использованная в другой части кода. С этой целью стандарт PDSIX требует объявить следующие именованные константы (symbolic constants) в <unistd. h>: STDIN_FILENO «Номер файла» для стандартного ввода: 0. STDOUT_FILENO Номер файла для стандартного вывода: 1. STDERR_FILENO Номер файла для стандартной ошибки: 2. Однако, по нашему скромному мнению, использование этих макросов избыточно. Во-первых, неприятно набирать 12 или 13 символов вместо 1. Во-вторых, использование 0, 1 и 2 так стандартно и так хорошо известно, что на самом деле нет никаких оснований для путаницы в смысле этих конкретных символических констант. С другой стороны, использование этих констант не оставляет сомнений в намерениях программиста. Сравните это утверждение: int fd = 0; Инициализируется ли f d значением стандартного ввода, или же программист благоразумно инициализирует свои переменные подходящим значением? Вы не можете этого сказать. Один из подходов (рекомендованный Джеффом Колье (Geoff Collyer)) заключается в использовании следующего определения enum: enum { Stdin, Stdout, Stderr }; Затем эти константы можно использовать вместо 0,1 и 2. Их легко читать и печатать.
4.4. Ввод и вывод 101 4.4.2. Открытие и закрытие файлов Новые дескрипторы файлов получают (наряду с другими источниками) в результате системного вызова ореп(). Этот системный вызов открывает файл для чтения или записи и возвращает новый дескриптор файла для последующих операций с этим файлом. Мы видели объявление раньше: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> ttinclude•<fcntl,h> #include <unistd.h> int open(const char ^pathname, int flags, mode_t mode); Три аргумента следующие: const char *pathname Строка С, представляющая имя открываемого файла. int flags Поразрядное ИЛИ с одной или более констант, определенных в <fcntl. h>. Вскоре мы их рассмотрим. mode_t mode Режимы доступа для создаваемого файла. Это обсуждается далее в главе, см. раздел 4.6. «Создание файлов». При открытии существующего файла опустите этот параметр1. Возвращаемое open () значение является либо новым дескриптором файла, либо -1, означающим ошибку, в этом случае будет установлена errno. Для простого ввода/вывода аргумент flags должен быть одним из значений из табл. 4.3. Таблица 4.3. Значения flags для ореп.() Именованная константа 0_RDONLY 0_WRONLY 0_RDWR Значение 0 1 2 Комментарий Открыть файл только для чтения; запись невозможна. Открыть файл только для записи; чтение невозможно. Открыть файл для чтения и записи. Вскоре мы увидим пример кода. Дополнительные значения flags описаны в разделе 4.6 «Создание файлов». Большой объем ранее написанного кода Unix не использовал эти символические значения. Вместо этого использовались числовые значения. Сегодня это рассматривается как плохая практика, но мы представляем эти значения, чтобы вы их распознали, если встретитесь с ними. Системный вызов closet) закрывает файл: его элемент в системной таблице дескрипторов файлов помечается как неиспользуемый, и с этим дескриптором нельзя производить никаких дальнейших действий. Объявление следующее: open () является одним из немногих варьирующих (variadic) системных вызовов. -Примеч. автора.
102 Глава 4. Файлы и файловый ввод/вывод #include <unistd.h> /* POSIX */ int close(int fd); В случае успеха возвращается 0, при ошибке (-1). При возникновении ошибки нельзя ничего сделать, кроме сообщения о ней. Ошибки при закрытии файлов являются необычными, но не невозможными, особенно для файлов, доступ к которым осуществляется через сеть. Поэтому хорошей практикой является проверка возвращаемого значения, особенно для файлов, открытых для записи. Если вы будете игнорировать возвращаемое значение, специально приведите его к типу void, чтобы указать, что вам не нужен результат: (void) close(fd); /* отказ от возвращаемого значения */ Легкомысленность этого совета в том, что слишком большое количество приведений к void имеют тенденцию загромождать код. Например, несмотря на принцип «всегда проверять возвращаемое значение», чрезвычайно редко можно увидеть код, проверяющий возвращаемое значение print f () или приводящий его к void. Как и со многими аспектами программирования на С, здесь также требуются опыт и рассудите?!ьность. Как упоминалось, число открытых файлов, если оно большое, ограничивается, и вам всегда следует закрывать файлы, когда работа с ними закончена. Если вы этого не сделаете, то в конечном счете выйдете за пределы лимита дескрипторов файлов, создав ситуацию, которая ведет к потере устойчивости части вашей программы. Система закрывает все открытые файлы, когда процесс завершается, но - за исключением 0, 1 и 2 - плохая манера полагаться на это. Когда open () возвращает новый дескриптор файла, она всегда возвращает наименьшее неиспользуемое целое значение. Всегда. Поэтому, если открыты дескрипторы файлов 0-6 и программа закрывает дескриптор файла 5, следующий вызов open () вернет 5, а не 7. Это поведение важно; далее в книге мы увидим, как оно используется для аккуратной реализации многих важных особенностей Unix, таких, как перенаправление ввода/вывода и конвейеризация (piping). 4.4.2.1. Отображение переменных file* на дескрипторы файлов Стандартные библиотечные функции ввода/вывода и переменные FILE * из <s tdio. h>, такие, как stdin, stdout и stderr, построены поверх основанных на дескрипторах файлов системных вызовах. Иногда полезно получить непосредственный доступ к дескриптору файла, связанному с указателем файла <s tdio. h>, если вам нужно сделать что-либо, не определенное стандартом С ISO. Функция f ileno () возвращает лежащий в основе дескриптор файла: #include <stdio.h>* /'* POSIX */ int fileno(FILE *stream); Пример мы увидим позже, в разделе 4.4.4. «Пример: Unix cat».
4.4. Ввод и вывод 103 4.4.2.2. Закрытие всех открытых файлов Открытые файлы наследуются порожденными процессами от своих родительских процессов. Фактически они являются общими. В частности, общим является положение в файле. Подробности мы оставим для дальнейшего обсуждения в разделе 9.1.1.2 «Разделение дескрипторов файлов». Поскольку программы могут наследовать другие файлы, иногда вы можете увидеть программы, которые закрывают все свои файлы, чтобы начать с «чистого состояния». В частности, типичен код наподобие этого: int i ; /* оставить лишь 0, 1, и 2 */ for (i = 3; i < getdtablesize(); i++) (void) close(i); Предположим, что результат getdtablesize () равен 1024. Этот код работает, но он делает A024-3)*2 = 2042 системных вызова. 1020 из них не нужны, поскольку возвращаемое значение getdtablesize () не изменяется. Вот лучший вариант этого кода: int i, fds; for (i = 3, fds = getdtablesize(); i < fds; i++) (void) close(i) ; Такая оптимизация не ухудшает читаемость кода, но может быть заметна разница, особенно на медленных системах. В общем, стоит поискать случаи, когда в циклах повторно вычисляется один и тот же результат, чтобы посмотреть, нельзя ли вынести вычисление за пределы цикла. Хотя в таких случаях нужно убедиться, что вы (а) сохраняете правильность кода и (б) сохраняете его читаемость! 4.4.3. Чтение и запись Ввод/вывод осуществляется системными вызовами read () и write () соответственно: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); Каждая функция сделана как можно проще. Аргументами являются дескриптор открытого файла, указатель на буфер для чтения или записи данных и число читаемых или записываемых байтов. Возвращаемое значение является числом действительно прочитанных или записанных байтов. (Это число может быть меньше запрошенного: при операции чтения это происходит, когда в файле осталось меньше count байтов, а при операции записи это случается, когда диск заполнен или произошла еще какая-нибудь ошибка.) Возвращаемое
104 Глава 4. Файлы и файловый ввод/вывод значение -1 означает возникшую ошибку, в этом случае errno указывает эту ошибку. Когда read () возвращает 0, это означает, что достигнут конец файла. Теперь мы можем показать оставшуюся часть кода для ch04-cat. Процедура process () использует 0 для стандартного ввода, если именем файла является «-» (строки 50 и 51). В противном случае она открывает данный файл: 36 /* 37 * process сделать что-то с файлом, в данном случае, 38 * послать его в stdout (fd l). 39 * Возвращает 0, если все нормально; в противном случае 1. 40 */ 41 42 int 43 process(char *file) 44 { 45 int fd; 46 ssize_t rcount, wcount ; 47 char buffer[BUFSIZ]; 48 int errors = 0; 49 50 if (strcmp(file, "-") == 0) 51 fd = 0; 52 else if ((fd = open(file, 0_RDONLY)) < 0) { 53 fprintf(stderr, "%s: %s: cannot open for reading: %s\n", 54 myname, file, s tr err or (errno) ) ; 55 return 1; 56 } Буфер buffer (строка 47) имеет размер BUFSIZ; эта константа определена B<stdio.h> как «оптимальный» размер блока для ввода/вывода. Хотя значение BUFSIZ различается в разных системах, код, использующий эту константу, чистый и переносимый. Основой процедуры является следующий цикл, который повторно читает данные до тех пор, пока не будет достигнут конец файла или не возникнет ошибка: 58 while ((rcount = read(fd, buffer, sizeof buffer)) > 0) { 59 wcount = writed, buffer, rcount); 60 if (wcount != rcount) { 61 fprintf(stderr, "%s: %s: write error: %s\n", 62 myname, file, strerror(errno)); 63 errors++; 64 break; 65 } 66 } Переменные rcount и wcount (строка 45) имеют тип ssize__t, «знаковый size_t», который позволяет хранить в них отрицательные значения. Обратите внимание, что число
4.4. Ввод и вывод 105 байтов, переданное writeO, является значением, возвращенным read() (строка 59). Хотя мы хотим читать порциями фиксированного размера в BUFSIZ, маловероятно, что размер самого файла кратен BUFSIZ. При чтении из файла завершающей, меньшей порции байтов, возвращаемое значение указывает, сколько байтов buffer получили новые данные. В стандартный вывод должны быть скопированы только эти байты, а не весь буфер целиком. Условие 'wcount ! = rcount' в строке 60 является правильным способом проверки на ошибки; если были записаны некоторые, но не все данные, wcount будет больше нуля, но меньше rcount. В заключение process () проверяет наличие ошибок чтения (строки 68-72), а затем пытается закрыть файл. В случае (маловероятном) неудачного завершения close () (строка 75) она выводит сообщение об ошибке. Избежание закрытия стандартного ввода не является абсолютно необходимым в данной программе, но является хорошей привычкой при разработке больших программ, в случае, когда другой код где-то в другом месте хочет что-то с ним делать или если порожденная программа будет наследовать его. Последний оператор (строка 82) возвращает 1, если были ошибки, и 0 в противном случае. 68 if (rcount < 0) { , 69 fprintf(stderr, "%s: %s: read error: %s\n", 70 myname, file, strerror(errno)); 71 errors++; 72 } 73 74 if (fd != 0) { 75 if (close(fd) < 0) { 76 fprintf(stderr, "%s: %s: close error: %s\n", 77 myname, file, strerror(errno)); 7 8 errors++; 79 } 80 } 81 82 return (errors != 0); 83 } ch04-cat проверяет на ошибки каждый системный вызов. Хотя это утомительно, зато предоставляет устойчивость (или по крайней мере, ясность): когда что-то идет не так, ch04-cat выводит сообщение об ошибке, которое специфично настолько, насколько это возможно. В сочетании с errno и strerror () это просто. Вот все с ch04-cat, всего 88 строк кода! Для подведения итогов вот несколько важных моментов, которые нужно понять относительно ввода/вывода в Unix: Ввод/вывод не интерпретируется Системные вызовы ввода/вывода просто перемещают байты. Они не интерпретируют данные; вся интерпретация оставлена программе уровня пользователя. Это делает
106 Глава 4. Файлы и файловый ввод/вывод чтение и запись двоичных структур таким же простым, как чтение и запись строк текста (на самом деле, проще, хотя использование двоичных данных привносит проблемы переносимости). Ввод/вывод гибок За один раз вы можете прочесть или записать столько байтов, сколько захотите. Вы можете даже читать или записывать данные по одному байту за раз, хотя для больших объемов данных это обходится дороже, чем использование больших порций. Ввод/вывод прост Три уровня возвращаемых значений (отрицательные для ошибок, ноль для конца файла и положительные для счета) делают программирование простым и очевидным. Ввод/вывод может быть частичным Как read (), так и write () могут переместить меньше байтов, чем запрошено. Код приложения (т. е. ваш код) всегда должен учитывать это. 4.4.4. Пример: Unix cat Как и было обещано, вот версия cat V72. Она начинается с проверки опций, cat V7 принимает единственную опцию, -и, для осуществления не буферированного вывода. Общая структура сходна с той, которую мы видели ранее; программа перечисляет файлы, указанные в аргументах командной строки и читает каждый файл, по одному символу за раз, посылая этот символ в стандартный вывод. В отличие от нашей версии, она использует возможности <stdio.h>. Во многих случаях код, использующий стандартную библиотеку ввода/вывода, проще читать и писать, поскольку все проблемы с буферами скрыты библиотекой. 1 /* 2 * Объединение файлов. 3 */ 4 5 ttinclude <stdio.h> 6 #include <sys/types.h> 7 #include <sys/stat.h> 8 9 char stdbuf[BUFSIZ]; 10 11 main(argc, argv) /* int main(int argc, char **argv) */ 12 char **argv; 13 { 2 Cm. /usr/src/cmd/cat .с в дистрибутиве V7. Программа без изменений компилируется для GNU/ Linux. - Примеч. автора.
4.4. Ввод и вывод 107 14 int fflg = 0; 15 register FILE *fi; 16 register c; 17 int dev, ino = -1; 18 struct stat statb; 19 20 setbuf(stdout, stdbuf); 21 for ( ; argol && argv[l] [0] == ' - ' ; argc--,argv++) { 22 switch(argv[1][1]) { У* Обработка опций */ 23 case 0: 2 4 break; 25 case 'u' : 26 setbuf(stdout, (char *)NULL); 27 continue; 28} 2 9 break; 30 } < ' 31 fstat (f ileno (stdout) ,&statb); /* Строки 31-36 объясняются в главе 5 */ 32 statb.st_mode &= S_IFMT; 33 if (statb.st_mode!=S_IFCHR && statb.st_mode!=S__IFBLK) { 34 dev = statb.st_dev; 3 5 ino = statb.st_ino; 36 } ' 37 if (argc < 2) { 3 8 argc = 2; 39 fflg++; 40 } 41 while (--argc > 0) { Loop over files 42 if (fflg ||¦(*++argv)[0]=='-' && (*argv)[1]=='\0') 43 fi = stdin; 44 else { 45 if ((fi = fopen(*argv, "r")) == NULL) { 46 fprintf(stderr, "cat: can't open %s\n", *argv); 47 continue; 48 } 49 } 50 fstat(fileno(fi), &statb); /* Строки 50-56 объясняются в главе 5 */ 51 if (statb. st_dev==dev && statb. st__ino==ino) { 52 fprintf(stderr, "cat: input %s is output\n", 53 fflg?"-": *argv); 54 fclose(fi); 55 continue; 56 - }
108 Глава 4. Файлы и файловый ввод/вывод 57 while((c=getc(fi)) != EOF) /* Копировать содержимое в stdout */ 58 putchar(с); 59 if (fi!=stdin) 60 fclose(fi) ;, 61 } ' 62 return@); 63 } Следует заметить, что программа всегда завершается успешно (строка 62); можно было написать ее так, чтобы отмечать ошибки и указывать их в возвращаемом значении main (). (Механизм завершения процесса и значение различных кодов завершения обсуждаются в разделе 9.1.5.1 «Определение статуса завершения процесса».) Код, работающий с struct stat и функцией f stat () (строки 31-36 и 50-56), без сомнения, непрозрачен, поскольку мы еще не рассматривали эти функции и не будем рассматривать до Следующей главы. (Но обратите внимание на использование filenoO в строке 50 для получения нижележащего дескриптора файла, связанного с переменными FILE *.) Идея в основе этого кода заключается в том, чтобы убедиться, что входной и выходной файлы не совпадают. Это предназначено для предотвращения бесконечного роста файла, в случае подобной команды: $ cat myfile >> myfile /* Добавить копию myfile к себе? */ И конечно же, проверка работает: $ echo hi > myfile /* Создать файл */ $ v7cat myfile >> myfile /* Попытка добавить файл к себе */ cat: input myfile is output Если вы попробуете это с ch04-cat, программа продолжит работу, и myfile будет расти до тех пор, пока вы не прервете ее. GNU версия cat осуществляет эту проверку Обратите внимание, что что-то вроде этого выходит за рамки контроля cat: $ v7cat < myfile > myfile cat: input - is output $ Is -1 myfile -rw-r—r-- 1 arnold devel 0 Mar 24 14:17 myfile В данном случае это слишком поздно, поскольку оболочка урезала файл myfile (посредством оператора >) еще до того, как cat получила возможность исследовать файл! В разделе 5.4.4.2 «Возвращаясь к V7 cat» мы объясним код с struct stat. 4.5. Произвольный доступ: перемещения внутри файла До сих пор мы обсуждали последовательный ввод/вывод, при котором данные читаются или записываются с начала файла и продолжаются до его конца.. Часто это все, что требуется программе. Однако, возможно осуществление произвольного ввода/вы-
4.5. Произвольный доступ: перемещения внутри файла 109 вода; т. е. читать данные из произвольного положения в файле без необходимости предварительного чтения всего, что находится перед этим местом. Смещение дескриптора файла является положением внутри открытого файла, начиная с которого будет осуществляться следующая операция чтения или записи. Программа устанавливает смещение с помощью системного вызова lseek (): #include <sys/types.h> /* для off_t; POSIX */ #include <unistd.h> /* объявления lseek() и значений whence */ off_t lseek(int fd, off_t offset, int whence); Тип of f_t (тип смещения) является знаковым целым, представляющим позиции байтов (смещений от начала) внутри файла. На 32-разрядных системах тип представлен обычно как long. Однако, многие современные системы допускают очень большие файлы, в этом случае of f_t может быть более необычным типом, таким, как С99 int64_t или какой-нибудь другой расширенный тип. lseek () принимает три следующих аргумента: int fd Дескриптор открытого файла. off_t offset Позиция, в которую нужно переместиться. Интерпретация этого значения зависит от параметра whence, offset может быть положительным или отрицательным; отрицательные значения перемещают к началу файла; положительные значения перемещают к концу файла. int whence Описывает положение в файле, относительно которого отсчитывается offset. См. табл. 4.4. Таблица 4.4. Значения whence для lseek () Именованная константа SEEKJ3ET SEEK_CUR SEE,K__END Значение 0 1 2 Комментарий offset абсолютно, т.е. относительно начала файла. offset относительно текущей позиции в файле. offset относительно конца файла. Большое количество старого кода использует числовые значения, приведенные в табл: 4.4. Однако, любой новый код, который вы пишете, должен использовать символические имена, значение которых более ясно. Смысл значений и их действие на положение в файле показаны на рис. 4.1. При условии, что файл содержит 3000 байтов и что перед каждым вызовом lseek () текущим является смещение 2000 байтов, новое положение после каждого вызова будет следующим: Отрицательные смещения относительно, начала файла бессмысленны; они вызывают ошибку «недействительный параметр». Возвращаемое значение является новым положением в файле. Поэтому, чтобы получить ваше текущее местоположение в файле, используйте
110 Глава 4. Файлы и файловый ввод/вывод Начало файла: О Текущее t: 2000 Конец файла: offset 3000 Новое положение: 3040 2960 , 2040 I960 40 -lseek(fd, (off_t) 40, SEEKJ2NC lseek(fd, (off t) -40, SEEK END); —: lseek(fd lseek(fd, (off t) 40 lseek(fd/ (off t) 40, SEEK_CUR); (offJJ) -40, SEEK_CUR) ; SEEK_SET); Рис. 4.1. Смещения для lseek () off_t curpos; curpos = lseek(fd, (off_t) 0, SEEK_CUR); Буква 1 в lseek() означает long. lseek() был введен в V7 Unix, когда размеры файлов были увеличены; в V6 был простой системный вызов seek (). В результате большое количество старой документации (и кода) рассматривает параметр offset как имеющий тип long, и вместо приведения к типу of f_t довольно часто можно видеть суффикс L в константных значениях смещений: curpos = lseek(fd, 0L, SEEK_CUR); На.системах с компилятором стандартного С, где lseek () объявлена с прототипом, такой старый код продолжает работать, поскольку компилятор автоматически преобразует 0L из long в of f _?, если это различные типы. Одной интересной и важной особенностью lseek () является то, что она способна устанавливать смещение за концом файла. Любые данные, которые впоследствии записываются в это место, попадают в файл, но с образованием «интервала» или «дыры» между концом предыдущих данных файла и началом новых данных. Данные в промежутке читаются, как если бы они содержали все нули. Следующая программа демонстрирует создание дыр. Она записывает три экземпляра struct в начало, середину и дальний конец файла. Выбранные смещения (строки 16-18, третий элемент каждой структуры) произвольны, но достаточно большие для демонстрации особенности: /* ch04-holes.c Демонстрация lseek() и дыр в файлах. */ #include <stdio.h> /* #include <errno.h> /* #include <fcntl.h> /* #include <string.h> /H #include <unistd.h> /* для fprintfO, stderr, BUFSIZ объявление errno */ для flags для open() */ объявление strerrorO */ для ssize_t */ 4
4.5. Произвольный доступ: перемещения внутри файла 111 8 #include <sys/types.h> /* для off_t, etc. */ 9 #include <sys/stat.h> /* для mode_t */ 10 11 struct person { 12 char name[10]; /* имя */ 13 char id[10]; /* идентификатор */ 14 off_t pos; /* положение в файле для демонстрации */ 15 } people[] = { 16 { "arnold", 23456789", О-}, 17 { "miriam", "987654321", 10240 }., 18 { "joe", 92837465", 81920 }, 19 }; 20 21 int 22 main (int argc, char **argv)' 23 { 24 int fd; 2 5 int i, j ; 26 27 if (argc < 2) { 28 fprintf(stderr, "usage: %s file\n", argv[0]); 29 return 1; 30 } 31 32 fd = open(argv[l] , CL.RDWR | 0_CREAT | 0_TRUNC, 0666); 33 if (fd < 0) { 34 fprintf(stderr, "%s: %s: cannot open for read/write: %s\n", 3 5 argv[0], argv[l], strerror(errno)); 3 6 return 1; 37 } 38 39 j = sizeof(people) / sizeof(people[0]); /* число элементов */ Строки 27-30 гарантируют, что программа была вызвана правильно. Строки 32-37 открывают именованный файл и проверяют успешность открытия. Вычисление числа элементов j массива в строке 39 использует отличный переносимый трюк: число элементов является размером всего массива, поделенного на размер первого элемента. Красота этого способа в том, что он всегда верен: неважно, сколько элементов вы добавляете в массив или удаляете из него, компилятор это выяснит. Он не требует также завершающей сигнальной метки; т. е. элемента, в котором все поля содержат нули, NULL или т. п.
112 Глава 4. Файлы и файловый ввод/вывод Работа осуществляется в цикле (строки 41-55), который отыскивает смещение байтов, приведенное в каждой структуре (строка 42), а затем записывает всю структуру (строка 49): 41 for (i = 0; i < j ;.,! + +) { 42 if (lseek(fd, people[i].pos, SEEK_SET) < 0) { 43 fprintf(stderr, "%s: %s: seek error: %s\nM, 44 argv[0], argv[l], strerror(errno)); 45 (void) close(fd); 46 return 1; 47 } 48 49 if (write(fd, &people[i], sizeof(people[i])) != sizeof(people[i])) { 50 fprintf(stderr, "%s: %s: write error: %s\n", 51 argv[0], argv[l], strerror(errno)); 52 (void) close(fd); 53 return 1; 54 } 55 } 56 57 /* здесь все нормально */ 58 (void) close(fd); 59 return 0; 60 } Вот результаты запуска программы: $ ch04-holes peoplelist /* Запустить программу */ $ Is -Is peoplelist /* Показать использованные размеры и блоки */ 16 -rw~r--r-- 1 arnold devel 81944 Mar 23 17:43 peoplelist $ echo 81944 / 4096 | be -1 /* Показать блоки, если нет дыр */ 2 0.00585937 500000000000 Случайно мы знаем, что каждый дисковый блок файла использует 4096 байтов. (Откуда мы это знаем, обсужДается в разделе 5.4.2 «Получение информации о файле». Пока примите это как данное.) Финальная команда be указывает, что файлу размером 81944 байтов нужен 21 дисковый блок. Однако, опция -s команды Is, которая сообщает нам, сколько блоков использует файл на самом деле, показывает, что файл использует лишь 16 блоков!3 Отсутствующие блоки в файле являются дырами. Это показано на рис. 4.2. По крайней мере, три из этих блоков содержат данные, которые мы записали; другие для использования операционной системой при отслеживании размещения этих данных. - Примеч. автора.
4.6. Создание файлов 113 Дыры arnold J miriam 1 joe Блок: 1 3 21 U Логический размер »| Рис. 4.2. Дыры файла в файле ЗАМЕЧАНИЕ. ch04-holes.с не осуществляет непосредственный двоичный ввод/вывод. Это хорошо демонстрирует красоту ввода/вывода с произвольным доступом: вы можете рассматривать дисковый файл, как если бы он был очень большим массивом двоичных структур данных. На практике сохранение данных путем использования двоичного ввода/вывода является решением, которое необходимо тщательно взвесить. Например, что если предположить, что вам нужно переместить данные на систему, использующую отличный порядок байтов для целых? Или другие форматы чисел с плавающей точкой? Или на систему с другими требованиями выравнивания? Игнорирование подобных вопросов может стать слишком дорогостоящим. 4.6. Создание файлов Как было описано ранее, open (), очевидно, открывает лишь существующие файлы. Данный раздел описывает, как создавать новые файлы. Есть две возможности: creat () и open () с дополнительными файлами. Первоначально creat () был единственным способом создания файла, но затем эта возможность была добавлена также и к open (). Оба механизма требуют указания начальных прав доступа к файлу. 4.6.1. Определение начальных прав доступа к файлу Как пользователь GNU/Linux, вы знакомы с правами доступа к файлу, выдаваемыми командой 'Is -1': на чтение, запись и исполнение для каждого из владельца файла, группы и остальных. Различные сочетания часто выражаются в восьмеричной форме, в частности, для команд chmod и chmask. Например, права доступа к файлу -rw-r--r— эквивалентны восьмеричному 0644, a -rwxr-xr-x эквивалентно восьмеричному 0755. (Ведущий 0 в нотации С означает восьмеричные значения.) Когда вы создаете файл, вы должны знать, какую защиту необходимо назначить новому файлу. Вы можете сделать это с помощью простого восьмеричного числа, если захотите, и такие числа довольно обычно можно увидеть в старом коде. Однако, лучше
114 Глава 4. Файлы и файловый ввод/вывод использовать побитовую операцию OR для одной или более символических имен из <sys/stat ,h>, описанных в табл. 4.5. Таблица 4.5. Символические имена POSIX для режимов доступа к файлу Символическое имя Значение Комментарий S_IRWXU S_IRUSR S_IREAD S_IWUSR S_IWRITE S_IXUSR S_IEXEC S_IRWXG S_IRGRP S_IWGRP S__IXGRP S_IRWXO S_IROTH S_IWOTH S IXOTH 00700 00400 00200 00100 00070 00040 00020 00010 00007 00004 00002 00001 Разрешение на чтение, запись и исполнение для владельца Разрешение на чтение для владельца Аналогично S_IRUSR Разрешение на запись для владельца Аналогично SJWUSR Разрешение на исполнение для владельца. Аналогично SJXUSR Разрешение на чтение, запись и исполнение для группы. Разрешение на чтение для группы. Разрешение на запись для группы. Разрешение на исполнение для группы. Разрешение на чтение, запись и исполнение для остальных. Разрешение на чтение для остальных. Разрешение на запись для остальных. Разрешение на исполнение для остальных. Следующий фрагмент показывает, как создать переменные, представляющие разрешения -rw-r--r-- и -rwxr-xr-x @644 и 0755 соответственно): mode_t rw_mode, rwx__mode ; rw_mode = S_IRUSR | S__IWUSR | S^IRGRP | S_IROTH; /* 0644 */ rwx_mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; /* 07 55 */ Более старый код использовал S_IREAD, S_IWRITE и S_IEXEC вместе со сдвигом битов для получения того же результата: mode_t rw_mode/ rwx_mode; rw_mode = (S_IREAD| S_IWRITE) | (S__IREAD » 3) | (S_IREAD » 6); /* 0644 */ rwx_mode = (S_IREAD|S_IWRITE|S_IEXEC) | ((S_IREAD|S_IEXEC) » 3) | ((S_IREAD|S_IEXEC) » 6); /* 0755 */ К сЪжалению, ни одна из записей не является очень удобной. Современные версии предпочтительнее, поскольку у каждого бита доступа есть собственное имя и меньше вероятность неправильного выполнения побитовых операций.
4.6. Создание файлов 115 При изменении прав доступа к файлу для использования доступны биты дополнительных разрешений, показанные в табл. 4.6, но они не должны использоваться при первоначальном создании файла. Возможность включения этих битов широко варьирует между операционными системами. Лучше всего не пробовать; вместо этого следует изменить права доступа к файлу явным образом после его создания. (Изменение прав доступа описано в разделе 5.5.2 «Изменение прав доступа: chmod() и fchmod()». Значения этих битов обсуждаются в главе 11 «Права доступа и идентификаторы пользователя и группы».) Таблица 4.6. Дополнительные символические имена POSIX для режимов доступа к файлам Символическое имя Значение Смысл S__ISUID 04000 Установить ID пользователя. S_ISGID 02000 Установить ID группы. S_ISVTX 01000 Сохранить текст. Когда стандартные утилиты создают файлы, они по умолчанию используют права доступа -rw-rw-rw- (или 0666). Поскольку большинство пользователей предпочитают избегать файлов, в которые может записывать кто угодно, каждый процесс имеет при себе umask. umask является набором битов допуска, указывающим те биты, которые никогда не должны устанавливаться при создании новых фатов, (umask не используется при изменении прав доступа.) Концептуально осуществляется операция действительные_права = (затребованные_права & (-umask)); umask обычно устанавливается с помощью команды umask в $НОМЕ/ .profile, когда вы входите в систему. Из программы С она устанавливается с помощью системного вызова umask (): #include <sys/types.h> /* POSIX */ #include <sys/stat.h> mode_t umask (mode__t mask); Возвращается старое значение umask. Поэтому для определения текущей маски нужно установить новое значение, а затем восстановить старое (или изменить его при необходимости): mode_t mask = umask@); /* получить текущую маску */ (void) umask(mask); /* восстановить ее */ Вот пример работы umask на уровне оболочки: $ umask /* Показать текущую маску */ 0022 $ touch newfile /* Создать файл */ $ Is -1 newfile /* Показать права доступа нового файла */ -rw-r--r-- 1 arnold devel 0 Mar 24 15:43 newfile $ umask 0 /* Установить пустую маску */ $ touch newfile2 /* Создать второй файл */ $ Is -1 newfile2 /* Показать права доступа нового файла */ -rw-rw-rw- 1 arnold devel 0 Mar 24 15:44 newfile2
116 Глава 4. Файлы и файловый ввод/вывод 4.6.2. Создание файлов с помощью creat () Системный вызов creat (L создает новые файлы. Он объявлен следующим образом: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> #include <fcntl.h> int creat(const char *pathname, mode_t mode); Аргумент mode представляет права доступа к новому файлу (как обсуждалось в предыдущем разделе). Создается файл с именем pathname, с данными правами доступа, модифицированными с использованием umask. Он открыт (только) для чтения, а возвращаемое значение является дескриптором нового файла или -1, если была проблема. В последнем, случае errno указывает ошибку. Если файл уже существует, он будет при открытии урезан. Во всех остальных отношениях дескрипторы файлов, возвращаемые creat (), являются теми же самыми, которые возвращаются open (); они используются для записи и позиционирования и должны закрываться при помощи close (): int fd, count; /* Проверка ошибок для краткости опущена */ fd = creat("/some/new/file", 0666); count = write(fd, "some data\n", 10); (void) close(fd); 4.6.3. Возвращаясь к open () Вы можете вспомнить объявление для open (): int open(const char *pathname, int flags, mode__t mode); Ранее мы сказали, что при открытии файла для простого ввода/вывода мы можем игнорировать аргумент mode. Хотя, посмотрев Ha^creat (), вы, возможно, догадались, что open () также может использоваться для создания файлов и что в этом случае используется аргумент mode. Это в самом деле так. Помимо флагов 0_RDONLY, 0_WRONLY и 0_RDWR, при вызове open () могут добавляться с использованием поразрядного OR дополнительные флаги. Стандарт POSIX предоставляет ряд этих дополнительных флагов. В табл. 4.7 представлены флаги, которые используются для большинства обычных приложений. Если даны 0_APPEND и 0__TRUNC, можно представить, как оболочка могла бы открывать или создавать файлы, соответствующие операторам > и ». Например: int fd;• ' extern char * filename; mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; /* 0666 */ Да, это пишется так. Кена Томпсона (Ken Thompson), одного из двух «отцов» Unix, однажды спросили, что бы он сделал по-другому, если бы ему пришлось переделать Unix. Он ответил, что написал бы creat() с «е» на конце, И в самом деле, именно это он сделал для операционной системы Plan 9 From Bell Lobs. - Примеч. автора.
4.6. Создание файлов 117 fd = open (filename, 0_CREAT | 0_WRONLY | 0_TRUNC, mode); /* для > */ fd = open (filename, 0_CREAT j 0_WRONLY|0_APPEND, mode); /* для » */ Таблица 4.7. Дополнительные флаги POSIX для open () Флаг Значение 0_APPEND Принудительно осуществляет все записи в конец файла. 0_CREAT Создает новый файл, если он не существует. 0_EXCL При использовании вместе с 0_CREAT возвращает ошибку, если файл уже существует. 0__TRUNC Урезает файл (устанавливает его длину в 0), если он существует. Обратите внимание, что флаг OEXCL здесь не используется, поскольку как для >, так и для » не является ошибкой существование файла. Запомните также, что система применяет к запрошенным правам доступа umask. Также легко видеть, что, по крайней мере концептуально, creat () можно было бы легко написать следующим образом: int creat(const char *path, mode_t mode) { return open(path, 0_CREAT|0_WRONLY|0_TRUNC, mode) I ЗАМЕЧАНИЕ. Если файл открыт с флагом 0_APPEND, все данные будут записаны в конец файла, даже если текущее смещение было восстановлено с помощью lseek (). Современные системы предоставляют дополнительные флаги с более специализированным назначением. Они кратко описаны в табл. 4.8. Таблица 4.8. Дополнительные расширенные флаги POSIX для open () Флаг Значение 0_NOCTTY Если открываемое устройство является терминалом, оно не становится , управляющим терминалом процесса. (Это более сложная тема, кратко обсуждаемая в разделе 9.2.1.) CMTONBLOCK Запрещает в некоторых случаях блокирующие операции ввода/вывода (см. раздел 9.4.3.4). 0_DSYNC Гарантирует перенос записываемых в файл данных на физический носитель до возвращения write (). CL.RSYNC Гарантирует, что данные, которые читает read () и которые могли быть записаны в читаемый файл, были записаны на физический носитель до возвращения read(). 0_SYNC Аналогично 0__DSYNC, но дополнительно обеспечивается запись на физический носитель всех вспомогательных данных файла, таких, как время последнего доступа.
118 Глава 4. Файлы и файловый ввод/вывод Флаги 0_DSYNC, CMRSYNC и 0__SYNC требуют некоторых пояснений. Системы Unix (включая Linux) содержат внутренний кэш дисковых блоков, который называется буферным кэшем (buffer cache). Когда возвращается системный вызов write (), данные, переданные операционной системе, были скопированы в буфер в буферном кэше. Они необязательно были записаны на диск. Буферный кэш значительно повышает производительность: поскольку дисковый ввод/ вывод часто на порядок и медленнее операций центрального процессора и памяти, программы значительно снизили бы производительность, если бы им пришлось ждать завершения каждой записи на диск. Вдобавок, если данные были недавно записаны на диск, при последующем чтении тех же данных они уже находились бы в буферном кэше, откуда их можно вернуть немедленно, не дожидаясь завершения операции чтения с диска. Системы Unix осуществляют также опережающее чтение; поскольку чтение в большинстве случаев последовательное, операционная система после прочтения одного блока осуществляет чтение нескольких дополнительных последовательных блоков таким образом, что эта информация будет уже находиться в кэше, когда программа ее запросит. Если один и тот же файл читают несколько программ, они все получают преимущество, поскольку все получают свои данные из одной копии дисковых блоков файла в буферном кэше. Все это кэширование, конечно, замечательно, но бесплатного обеда не бывает. В то время, пока данные находятся в буферном кэше и до того, как они будут записаны на диск, есть небольшое, но вполне реальное окно, в котором может случиться катастрофа; например, если выключат питание. Современные дисковые приводы обостряют эту проблему: у многих из них есть собственные внутренние буферы, поэтому при записи данных на диск они могут оказаться не записанными на носитель при выключении питания! Это может быть значительной проблемой для небольших систем, которые не находятся в информационном центре с контролируемым энергоснабжением или не имеют источников бесперебойного питания (UPSM. Для большинства приложений вероятность того, что данные в буферном кэше могут быть нечаянно потеряны, довольно низка. Однако, для некоторых приложений любой такой шанс неприемлем. Поэтому в системе Unix было введено понятие синхронного ввода/вывода, при котором программе гарантируется, что по возвращении из системного вызова данные безопасно записаны на физическое устройство хранения. Флаг 0_DSYNC гарантирует целостность данных; данные и любая другая информация, которую операционная система должна найти, записываются на диск до возвращения write (). Однако, вспомогательные данные, такие, как время модификации или доступа к файлу, могут быть не записаны на диск. Флаг 0_SYNC требует, чтобы эти данные также были записаны на диск до возвращения write (). (Здесь тоже нет бесплатного обеда; синхронные записи могут серьезно повлиять на производительность программы, заметно её снизив.) Если у вас нет UPS и вы используете систему для критической работы, мы настоятельно рекомендуем вам обзавестись им. Следует также регулярно делать резервные копии. - Примеч. автора.
4,7. Форсирование записи данных на диск 119 <f^ar,0_RSYNC предназначен для чтения данных: если read() находит данные в буферном кэше, которые были назначены для записи на диск, функция не вернет эти данные до тех пор, пока они не будут записаны. Два других флага влияют на это: в частности, 0_SYNC заставит read () ждать, пока не будут также записаны и вспомогательные данные. ХЗЛМЕЧАНИЕ. Что касается ядра версии 2.4, Linux рассматривает все три флага {одинаково со значением флага 0_SYNC. Более того, Linux определяет дополнатель- \ные флаги, которые специфичны для Linux и предназначены для специального I использования. Дополнительные подробности см. в справочной странице GNU/Linux \дляорепB). 4.7. Форсирование записи данных на диск Ранее мы описали флаги CL.DSYNC, 0_RSYNC и 0_SYNC для open (). Мы отметили, что использование этих флагов может замедлить программу, поскольку write () не возвращается до тех пор, пока все данные не будут записаны на физический носитель. Со слегка более высоким уровнем риска мы можем сами испечь свое пирожное и съесть его. Это осуществляется путем открытия файла без указания флагов OxSYNC, но с последующим использованием одного из следующих двух системных вызовов в любой момент, когда это необходимо для безопасного перемещения данных на физический носитель: #include <unistd.h> int fsync(int fd) ; '" /* POSIX FSC */ int fdatasync(int fd); /* POSIX SIO */ Системный вь'вов f datasync () подобен 0_DSYNC: он форсирует запись данных на конечное физическое устройство. Системный вызов f sync () подобен 0_SYNC, форсируя запись на физическое устройство не только данных файла, но и вспомогательных данных. Вызов f sync () более переносим; он существовал в мире Unix в течение более продолжительного времени, и вероятность его наличия среди широкого ряда систем больше. Можно использовать эти вызовы с указателями файлов <stdio.h>, вызвав сначала f flush (), а затем f ileno() для получения дескриптора нижележащего файла. Вот функция f psync (), которая может использоваться для заключения обеих операций в один вызов. Она возвращает в случае успеха 0: /* fpsync синхронизация переменной stdio FILE * */ int fpsync(FILE *fp) { if (fp == NULL || fflush(fp) == EOF || fsync(fileno(fp)) < 0) return -1; return 0; } Технически оба этих вызова являются расширениями базового стандарта POSIX: f sync () в расширении «Синхронизация файлов» (FSC), a f datasync () в расширении «Синхронизированный ввод и вывод». Тем не менее, можно без проблем использовать их в системе GNU/Linux.
120 Глава 4. Файлы и файловый ввод/вывод 4.8. Установка длины файла Два системных вызова позволяют настраивать размер файла: #include <unistd.h> #include <sys/types.h> int truncate(const char *path, off_t length); /* XSI */ int ftruncate(int fd, off_t length); /* POSIX */ Как должно быть очевидно из параметров, truncate () принимает аргумент имени файла, тогда как f truncate () работает с дескриптором открытого файла. (Обычным является соглашение по именованию пар системных вызовов ххх() и fxxxx()y работающих с именами файлов и дескрипторами файлов. Мы увидим несколько примеров в данной и последующих главах.) В обоих случаях аргумент length является новым размером файла. Этот системный вызов происходит от 4.2 BSD Unix, и на ранних системах мог использоваться лишь для сокращения длины файла, отсюда и название. (Он был создан, чтобы облегчить реализацию операции урезания в Фортране.) На современных системах, включая Linux, имя является неправильным, поскольку с помощью этих вызовов можно также увеличить, а не только сократить длину файла. (Однако, POSIX указывает, что возможность увеличения размера файла относится к расширению XSI.) Для этих вызовов сокращаемый файл должен иметь разрешение на запись (для truncate ()), или должен быть открыт для записи (для f truncate ()). Если файл сокращается, все данные после нового конца файла теряются. (Поэтому вы не можете сократить файл, снова удлинить его и найти там первоначальные данные.) Если файл^ас- ширен, как в случае записи данных после lseek (), данные между старым концом файла и новым концом файла читаются как нули. Эти вызовы сильно отличаются от 4open( file, . . . |0_TRUNC, mode)', который полностью урезает файл, отбрасывая все его данные. Эти же вызовы просто устанавливают абсолютную длину файла в данное значение. Эти функции довольно специализированы; они используются лишь четыре раза во всем коде GNU Coreutils. Мы представляем пример использования f truncate () в разделе 5.5.3 «Изменение отметок времени: utime () ». 4.9. Резюме • Когда системный вызов завершается неудачей, он обычно возвращает -1, а в глобальной переменной errno устанавливается предопределенное значение, указывающее на проблему. Для сообщений об ошибках могут использоваться функции perror() и strerror().
Упражнения 121 • Доступ к файлам осуществляется через небольшие целые, которые называются дескрипторами. Дескрипторы файлов для стандартного ввода, стандартного вывода и стандартной ошибки наследуются от родительского процесса программы. Другие получаются через open () или с г eat (). Для их закрытия используется close (), a getdtablesize () возвращает разрешенное максимальное число открытых файлов. Значение umask (устанавливаемое с помощью umask ()) влияет на права доступа, получаемые новыми файлами при создании с помощью creat () или с флагом 0_CREAT для open (). Системные вызовы read() и write () соответственно читают и записывают данные. Их интерфейс прост. В частности, они не интерпретируют данные; файлы представлены линейными потоками байтов. Системный вызов lseek () осуществляет ввод/вывод с произвольным доступом: возможность перемещаться внутри файла. • Для синхронного ввода/вывода предусмотрены дополнительные флаги для open (), при этом данные записываются на физический носитель данных до возвращения write () или read(). Можно также форсировать запись данных на диск на управляемой основе с помощью f sync () или f datasync (). • Системные вызовы truncate () и f truncate () устанавливают абсолютную длину файла. (На более старых системах они могут использоваться лишь для сокращения длины файла; на современных системах они могут также увеличивать файл.) Упражнения 1. Используя лишь open (), read (), write () и close (), напишите простую программу сору, которая копирует файл, имя которого дается в первом аргументе, в файл с именем во втором аргументе. 2. Усовершенствуйте программу сору так, чтобы она принимала "-" в значении «стандартный ввод» при использовании в качестве первого аргумента и в значении «стандартный вывод» в качестве второго аргумента. Правильно ли работает 'сору - -'? 3. Просмотрите страничку справки для ргосE) на системе GNU/Linux. В частности, посмотрите подраздел^/. Выполните 4ls -1 /dev/ f d' и непосредственно проверьте файлы в /proc/self /fd. Если бы /dev/stdin и дружественные устройства были бы в ранних версиях Unix, как это упростило бы код для программы V7 cat? (Во многих других современных системах Unix есть каталог или файловая система /dev/fd. Если вы не используете GNU/Linux, посмотрите, что вы можете обнаружить в своей версии Unix.) 4. Даже если вы пока этого не понимаете, постарайтесь скопировать сегмент кода из V7 cat. с, который использует struct stat и функцию fstat (), в ch04-cat.c, чтобы она также сообщала об ошибке для 'cat file >> file'. 5. (Простое) Предположив наличие strerror (), напишите свою версию perror (). 6. Каков результат выполнения 'ulimit -п' на вашей системе?
122 Глава 4. Файлы и файловый ввод/вывод 7< Напишите простую версию программы umask, назвав ее myumask, которая принимает в командной строке восьмеричную маску. Используйте strtol () с основанием 8 для преобразования строки символов аргумента командной строки в целое значение. Измените umask с помощью системного вызова umask (). Откомпилируйте и запустите myumask, затем проверьте значение umask с помощью стандартной команды umask. Объясните результаты. (Подсказка: в оболочке Bash введите 4type umask'.) 8. Измените простую программу сору, которую вы написали ранее, для использования open () с флагом OSYNC. Используя команду time, сравните характеристики первоначальной и новой версии большого файла. 9. Мы сказали, что для f truncate () файл должен быть открыт для записи. Как можно открыть файл для записи, когда у самого файла нет доступа записи? 10. Напишите программу truncate, которая используется следующим образом: 'truncate длина_файпа\
Глава 5 Каталоги и служебные данные файлов В этой главе: • 5.1. Просмотр содержимого каталога .123 • 5.2. Создание и удаление каталогов 134 • 5.3. Чтение каталогов 136 • 5.4. Получение информации о файлах 143 • 5.5. Смена владельца, прав доступа и времени изменения 158 • 5.6. Резюме 164 • Упражнения 165 Данная глава продолжает подъем по кривой обучения до следующего плато: понимания каталогов и информации о файлах. В данной главе мы исследуем то, как информация хранится в каталоге, как читаются, создаются и удаляются сами каталоги, какая информация о файлах доступна и как ее получить. Наконец, мы рассмотрим другие способы обновления служебных данных файлов, таких, как владелец, группа, права доступа и время доступа и изменения файла. 5.1. Просмотр содержимого каталога Все Unix-системы, включая Linux, используют для хранения файловой информации на диске один и тот же концептуальный Дизайн. Хотя в реализации дизайнд есть значительные вариации, интерфейс на уровне С остается постоянным, давая возможность писать переносимые программы, которые компилируются и запускаются на многих различных системах.
124 Глава 5. Каталоги и служебные данные файлов 5.1.1. Определения ДРУЖЕСТВЕННЫЙ К ПОЛЬЗОВАТЕЛЮ по llliad Помните дни ДОСа с конфигурационными файлами? «Edit» был таким безнадежным текстовым редактором. Слабак. Я использовал edlin. Да? Ну а я писал свои файлы с помощью «echo: Ну а я редактировала индексы вручную. С помощью магнита. БОЛВАН! ЗАНУДА! Рис. Copyrigth 1997-2004 © J.D. «llliad» Frazer. Использовано по разрешению, http://www.userfriendly.org Мы начнем обсуждение с определения нескольких терминов. Раздел (partition) Единица физического хранилища. Физические разделы обычно являются либо частями диска, либо целым диском. Современные системы дают возможность создавать из нескольких физических логические разделы. Файловая система (filesystem) Раздел (физический или логический), содержащий данные файла и служебные данные (metadata)^ информацию о файлах (в противоположность содержимому файла, которое является информацией в файле). Такие служебные данные включают владельца файла, права доступа, размер и т. д., а также информацию, исполь- ч зующуюся операционной системой при поиске содержимого файла. Файловые системы размещаются «в» разделах (соотношение один к одному) посредством записи в них стандартной информации. Это осуществляется программой уровня пользователя, такой, как mke2f s в GNU/Linux или newf s в Unix. (Команда Unix mkf s создает разделы, но ее трудно использовать/Непосредственно, newfs вызывает ее с нужными параметрами. Если ваша система является системой Unix, подробности см. в справочных страницах для пе\ф($) и mkfs(8).) Большей частью GNU/Linux и Unix скрывают наличие файловых систем и разделов. (Дополнительные подробности приведены в разделе 8.1 «Монтирование и демонтирование файловых систем».) Доступ ко всему осуществляется через пути, безотносительно к тому, на каком диске расположен файл. (Сравните это с почти любой ком-
5.1. Просмотр содержимого каталога 125 мерческой операционной системой, такой, как OpenVMS, или с поведением по умолчанию любой системы Microsoft.) Индекс (inode) Сокращение от 'index node' (индексный узел), первоначально сокращалось 'i-node', а теперь пишется 'inode'. Небольшой блок информации, содержащий все сведения о файле, за исключением имени файла. Число индексов и, следовательно, число уникальных файлов в файловой системе, устанавливается и делается постоянным при создании файловой системы/Команда 'df -i' может показать, сколько имеется индексов и сколько из них используется. Устройство (device) В контексте файлов, файловых систем и служебных данных файлов, уникальный номер, представляющий используемую («смонтированную») файловую систему. Пара (устройство, индекс) однозначно идентифицирует файл: два различных файла будут гарантированно иметь различные пары (устройство, индекс). Более подробно это обсуждается далее в этой главе. Каталог (directory) Специальный файл, содержащий список пар (индекс, имя). Каталоги могут быть открыты для чтения, но не для записи; все изменения в содержимом каталога делает операционная система. Концептуально каждый дисковый блок содержит либо некоторое число индексов, либо данные файла. Индекс, в свою очередь, содержит указатели на блоки, содержащие данные файла. См. рис. 5.1. Дисковые блоки, линейные в разделе | г Г N ! ° Г D 1 Е I N О D Е I N О D Е I N О D Е i N.. о D. Е I ' N \ 0 1 D Е ! Г Data Data Data Data №«№S^«S - i *A' '-3»»U»?te.<№vV5е** >: %.wrse& !Л 't»,f ^J Рис. 5.1. Концептуальное представление индексов и блоков данных
126 Глава 5. Каталоги и служебные данные файлов На рисунке показаны все блоки индексов перед разделом и блоки данных после них. Ранние файловые системы Unix были организованы именно таким способом. Однако, хотя все современные системы до сих пор содержат индексы и блоки данных, их организация для повышения эффективности и устойчивости была изменена. Детали меняются от системы к системе, и даже в рамках систем GNU/Linux имеется множество разновидностей файловых систем, но концепция остается той же самой. 5.1.2. Содержимое каталога Каталоги устанавливают связь между именем файла и индексом. Элементы каталога содержат номер индекса и имя файла. Они содержат также дополнительную учетную информацию, которая нам здесь не интересна. См. рис. 5.2. На ранних Unix-системах были двухбайтные номера индексов, а имена файлов - до 14 байтов. Вот полное содержание файла V7 /usr/include/sys/dir .h: 23 f . 1 Точка 19 r .. | Точка-точка 42 [ guide 1 Имя файла .._„[___„„.__„ I О [ tempdata | Пустой элемент 37 [ .profile 1 Имя файла '—г 1 , Рис. 5.2. Концептуальное содержание каталога #ifndef DIRSIZ #define DIRSIZ 14 #endif struct direct { ino_t d__ino; char d_name [DIRSIZ] ; } ; ino_t определен в V7 <sys/types. h>KaK'typedef unsigned int into__t;\ Поскольку на PDP-11 int является 16-разрядным, таким же является и ino_t. Такая организация упрощала непосредственное чтение каталогов; поскольку размер элемента был фиксирован, код был простым. (Единственно, за чем нужно было следить, это то, что полное 14- символьное d_name не завершалось символом NUL.)
5.1. Просмотр содержимого каталога 127 Управление содержанием каталога для системы также было простым. Когда файл удалялся из каталога, система заменяла номер индекса двоичным нулем, указывая, что элемент каталога не используется. Новые файлы могли потом использовать пустой элемент повторно. Это помогало поддерживать размер самих файлов каталогов в приемлемых рамках. (По соглашению, номер индекса 1 не используется; первым используемым индексом всегда является 2. Дополнительные сведения приведены в разделе 8.1 «Монтирование и демонтирование файловых систем».) Современные системы предоставляют длинные имена файлов. Каждый элемент каталога имеет различную длину, с обычным ограничением для компонента имени файла каталога в 255 байтов. Далее мы увидим, как читать на современных системах содержимое каталога. Также в современных системах номера индексов 32 (или даже 64!) разрядные. 5.1.3. Прямые ссылки Когда файл создается с помощью open () или creat (), система находит не использующийся индекс и присваивает его новому файлу. Она создает для файла элемент каталога с именем файла и номером индекса. Опция -i команды Is отображает номер индекса: $ echo hello, world > message /* Создать новый файл */ $ Is -il message /* Показать также номер индекса */. 228786 -rw-r--r— 1 arnold devel 13 May 4 15:43 message Поскольку элементы каталога связывают имена файлов с индексами, у одного файла может быть несколько имен. Каждый элемент каталога, ссылающийся на один и тот же индекс, называется ссылкой (link) или прямой ссылкой (hard link) на файл. Ссылки создаются с помощью команды In. Она используется следующим образом: 'In старый_файл новый__файл': $ In message msg /* Создать ссылку */ $ cat msg /*'Показать содержание нового имени */ hello, world $ Is -il msg message /* Показать номера индексов */ 228786 -rw-r—г— 2 arnold devel 13 May 4 15:43 message 228786 -rw-r—r— 2 arnold devel 13 May 4 15:43 msg Вывод показывает, что номера индексов двух файлов одинаковые, а третье поле расширенного вывода теперь равно 2. Это поле показывает счетчик ссылок, указывающий, сколько имеется ссылок (элементов каталога, ссылающихся на данный индекс) на данный файл. Нельзя не подчеркнуть: прямые ссылки все относятся к одному и тому же файлу Если вы измените один файл, изменятся и все остальные: $ echo "Hi# how ya doin1 ?" > msg /* Изменить файл через новое имя */ $ cat message /* Показать содержание через старое имя */ Hi, how ya doin' ? $ Is -il message msg /* Отобразить сведения. Размер изменился */ 228786 -rw-r--r— 2 arnold devel 19 May 4 15:51 message 228786 -rw-r—r~ 2 arnold devel 19 May 4 15:51 msg
128 Глава 5. Каталоги и служебные данные файлов Хотя мы создали две ссылки на один файл в одном каталоге, прямые ссылки не обязательно должны находиться в одном и том же каталоге; они могут находиться в любом каталоге в той же самой файловой системе. (Несколько подробнее это обсуждается в разделе 5.1.6 «Символические ссылки».) Вдобавок, вы можете создать ссылку на файл, который вам не принадлежит, если у вас есть право записи в каталоге, в котором вы создаете ссылку. (Такой файл сохраняет все атрибуты первоначального файла: владельца, права доступа и т. д. Это потому, что это и есть оригинальный файл; просто он получил дополнительное имя.) Код уровня пользователя не может создать прямую ссылку на каталог. После удаления ссылки создание еще одного файла с прежним именем создает новый файл: $ rm message /* Удалить старое имя */ $ echo "What's happenin?" > message /* Повторно использовать имя */ $ Is -il msg message /* Отобразить сведения */ 228794 -rw-r—г— 1 arnold devel 17 May 4 15:58 message 228786 -rw-r-.-r— 1 arnold devel 19 May 4 15:51 msg Обратите внимание, что теперь счетчик ссылок каждого из файлов равен 1. На уровне С ссылки создаются с помощью системного вызова link () : #include <unistd.h> /* POSIX */ int link(const char *oldpath, const char *newpath); При успешном создании ссылки возвращается 0, в противном случае (-1), при этом errno отражает ошибку. Важным- случаем ошибки является тот, когда newpath уже существует. Система не удалит его для вас, поскольку попытка сделать это может вызвать несовместимости в файловой системе. 5.1.3.1. Программа GNU link Программа In сложная и большая. Однако, GNU Coreutils содержит несложную программу link, которая просто вызывает link() со своими двумя аргументами. Следующий пример показывает код из файла link. с, не относящиеся к делу части удалены. Номера строк относятся к действительному файлу. 2 0 /* Обзор реализации: 21 22 Просто вызывает системную функцию 'link' */ 23 /* ...Операторы #include для краткости опущены... */ 34 35 /* Официальное имя этой програмы (например, нет префикса vg'). */ 36 #define PROGRAM_NAME "link" 37 38 #define AUTHORS "Michael Stone" 39
5.1. Просмотр содержимого каталога 40 /* Имя, под которым была запущена данная программа. */ 41 char *program__name; 43 void 44 usage (int status) 45 { /* ... для краткости опущено... */ 62 } ' 63 64 int 65 main (int argc, char **argv) 66 { 67 program_name = argv[0]; 68 setlocale (LC_ALL, ""); 69 bindtextdomain (PACKAGE, LOCALEDIR); 7 0 textdomain (PACKAGE); 71 72 atexit (close_stdout); 73 74 parse_long_options (argc, argv, PROGRAM_NAME, GNU_PACKAGE, 75 VERSION,AUTHORS, usage); 76 77 /* Вышеприведенное обрабатывает --help и --version. 78 Поскольку других вызовов getopt нет, обработать здесь х--' 79 if A < argc && STREQ (argv[l], "--")) 80 { 81 --argc; 82 ++argv; 83 } 84 85 if (argc < 3) 86 { 87 error @, 0, _("too few arguments")); 88 usage (EXIT_FAILURE); 89 } 90 91 if C < argc) 92 { 93 error @, 0, _("too many arguments")); 94 usage (EXIT_FAILURE); 95 } 96 97 if (link (argv[l], argv[2]) != 0)
130 Глава 5. Каталоги и служебные данные файлов 98 error (EXIT_FAILURE, errno, _("cannot create link %s to %s"), 949 quote_n @, argv [ 2 ] ) , quote_n A, argv [ 1 ] ) ) ; 100 101 exit (EXIT_SUCCESS); 102 } - Строки 67-75 являются типичным шаблоном Coreutils, устанавливающими интернациональные настройки, выход по завершении и анализ аргументов. Строки 79-95 гарантируют, что link вызывается лишь с двумя аргументами. Сам системный вызов link () осуществляется в строке 97. (Функция quote_n () обеспечивает отображение аргументов в стиле, подходящем для текущей локали; подробности сейчас несущественны.) 5.1.3.2. Точка и точка-точка Завершая обсуждение ссылок, давайте взглянем на то, как обрабатываются специальные имена '.' и 4. .'. На самом деле они просто являются прямыми ссылками. В первом случае '.' является прямой ссылкой на каталог, содержащий ее, а '. .' - прямой ссылкой на родительский каталог. Операционная система создает для вас эти ссылки; как упоминалось ранее, код уровня пользователя не может создать прямую ссылку на каталог. Этот пример иллюстрирует ссылки: $ pwd /* Отобразить текущий каталог */ /tmp $ Is -ldi /tmp /* Показать номер его индекса */ 225345 drwxrwxrwt 14 root root 4096 May 4 16:15 /tmp $ mkdir x /* Создать новый каталог */ $ Is -ldi x /* И показать номер его индекса */ 52794 drwxr-xr-x 2 arnold devel 4096 May 4 16:27 x $ Is -ldi x/• x/.. /* Показать номера индексов . и . . */ 52794 drwxr-xr-x 2 arnold devel 4096 May 4 16:27 x/. 225345 drwxrwxrwt 15 root root 4096 May 4 16:27 x/.. Родительский каталог корневого каталога (/ . .) является особым случаем; мы отложим его обсуждение до главы 8 «Файловые системы и обход каталогов». 5.1.4. Переименование файлов При данном способе отображения элементами каталога имен на номера индексов, переименование файла концептуально очень просто: 1. Если новое имя файла обозначает существующий файл, сначала удалить этот файл. 2. Создать новую ссылку на файл через новое имя. 3. Удалить старое имя (ссылку) для файла. (Удаление имен обсуждается в следующем разделе.) Ранние версии команды mv работали таким способом. Однако, при таком способе переименование файла не является атомарным; т. е. оно не осуществляется посредством одной непрерываемой операции. И на сильно загруженной системе злонамеренный поль-
5.1. Просмотр содержимого каталога 131 зователь мог бы воспользоваться условиями состояния гонки , разрушая операцию переименования и подменяя оригинальный файл другим. По этой причине 4.2 BSD ввело системный вызов rename (): #include <stdio.h> /* ISO С */ int renamed const char *oldpath, const char *newpath); На системах Linux операция переименования является атомарной; справочная страница утверждает: Если newpath уже существует, он будет атомарно замещен ..., таким образом, что при попытке другого процесса получить доступ к newpath он никогда не обнаружит его отсутствующим. Если newpath существует, но по какой-либо причине операция завершается неудачей, rename гарантирует, что экземпляр newpath останется на месте. Однако, в ходе переписывания возможно будет окно, в котором как oldpath, так и newpath ссылаются на переименовываемый файл. х Как и в случае с другими системными вызовами, возвращенный 0 означает успех, а (-1) означает ошибку. 5.1.5. Удаление файла Удаление файла означает удаление элемента каталога для файла и уменьшение счетчика ссылок на файл , который содержится в индексе. Содержимое файла и дисковые блоки, в котором оно размещается, не освобождаются до тех пор, пока счетчик ссылок не достигнет нуля. Системный вызов называется unlink (): #include <unistd.h> /* POSIX */• int unlink(const char ^pathname); В нашем обсуждении ссылок на файлы имя имеет смысл; этот вызов удаляет данную ссылку (элемент каталога) для файла. Она возвращает 0 в случае успеха и -1 при ошибке. Возможность удаления файла требует права записи лишь для каталога, а не для самого файла. Этот факт может сбивать с толку, особенно начинающих пользователей Linux/Unix. Однако, поскольку операция в каталоге одна, это имеет смысл; меняется именно содержимое каталога, а не содержимое файла2. Состояние гонки (race condition) является ситуацией, при которой детали временных соотношений могут вызывать непреднамеренные побочные эффекты или ошибки. В данном случае, каталог в течение короткого периода времени находится в противоречивом состоянии, и именно эта противоречивость и создает уязвимость. - Примеч. автора. Конечно, меняются служебные данные файла (число ссылок), но это не влияет ни на какой другой атрибут файла, также, как не влияет на содержимое файла. Обновление счетчика ссылок на файл является единственной операцией над файлом, при которой не проверяются права доступа к файлу. - Примеч. автора. к*
132 Глава 5. Каталоги и служебные данные файлов 5.1.5.1. Удаление открытых файлов С самых первых дней Unix было возможно удалять открытые файлы. Просто вызовите unlink () с именем файла после успешного вызова open () или creat (). На первый взгляд, это кажется странным. Поскольку система освобождает блоки данных, когда счетчик ссылок на файл достигает нуля, возможно ли использовать открытый файл? Ответ- да, вы можете продолжить использовать открытый файл обычным способом. Система знает, что файл открыт, поэтому она откладывает освобождение хранилища файла до тех пор, пока не будет закрыт последний дескриптор файла. Когда файл вообще больше не используется, память освобождается. Эта операция также оказывается полезной: это простой способ для программы получить временный файл, который гарантированно будет и конфиденциальным, и автоматически освобождаемым по завершении использования: /* Получение конфиденциального временного хранилища, проверка ошибок для краткости опущена */ int fd; mode_t mode = 0_CREAT | 0_EXCL | 0_TRUNC | 0__RDWR; • , . fd = open("/tmp/myfile", mode, 0000); /* Открыть файл */ unlink ("/tmp/my file") ; /* Удалить его '*/ /*'... продолжить использование файла... */¦ close(fd); /* Закрыть файл, освободить память */ Недостатком такого подхода является то, что вышедшее из-под контроля приложение может заполнить систему открытыми, но анонимными файлами, в этом случае администраторам придется попытаться найти и завершить этот процесс. В прежние дни могли потребоваться перезагрузка и проверка целостности файловой системы; к счастью, на современных системах это требуется исключительно редко. 5.1.5.2. Использование ISO С: remove () ISO С предоставляет для удаления файлов функцию remove (); она предназначена в качестве общей функции, годной для любой системы, поддерживающей ISO С, а не только для Unix и GNU/Linux: #include <stdio.h> /* ISO С */ int remove(const char *pathname); Хотя технически это не системный вызов, возвращаемое значение в том же стиле: 0 в случае успеха и -1 при ошибке, причем errno содержит значение ошибки. В GNU/Linux remove() использует для удаления файлов системный вызов unlink (), а для удаления каталогов. - системный вызов rmdir () (обсуждаемый далее в главе). (На более старых системах GNU/Linux, не использующих GLEBC, remove () является псевдонимом для unlink (); поэтому для каталогов завершается неудачей. Если у вас такая система, вам, возможно, следует ее обновить.)
5.1. Просмотр содержимого каталога 133 5.1.6. Символические ссылки Мы начали главу с обсуждения разделов, файловых систем и индексов. Мы также увидели, что элементы каталога связывают имена с номерами индексов. Поскольку элементы каталога не содержат другой информации, прямые ссылки ограничены файлами внутри одной и той же файловой системы. Это должно быть; нет способа отличить индекс 2341 одной файловой системы от индекса 2341 другой файловой системы. Вот что случится, если мы попытаемся это сделать: $ mount /* Показать использующиеся файловые системы *•/ /dev/hda2 on / type ext3 (rw) /dev/hda5 on /d type ext3 (rw) $ Is -li /tmp/message /* Предыдущий пример был в файловой системе / */ 228786 -rw-r—г— 2 arnold devel 19 May 4 15:51 /tmp/message $ cat /tmp/message Hi, how ya doin' ? $ /bin/pwd /* Текущий каталог в другой файловой системе */ /d/home/arnold $ In /tmp/message . /* Попытка создать ссылку */ In: creating hard link './message' to-V trap/message': Invalid cross-device link На больших системах часто много разделов как на физически подключенных локальных дисках, так и на удаленно смонтированных файловых системах. Ограничение прямых ссылок одной и той же файловой системой неудобно, например, если некоторые файлы или каталоги должны быть перемещены в новое место, но старое программное обеспечение использует жестко вшитые в код имена файлов для старых местоположений. Чтобы обойти это ограничение, 4.2 BSD ввело символические ссылки (symbolic links, называемые также soft links). Символическая ссылка является особой разновидностью файла (также, как особой разновидностью файла является каталог). Содержимое этого файла представляет собой путь к файлу, на который данный файл «указывает». Все современные Unix-системы, включая Linux, предусматривают символические ссылки; конечно, они теперь являются частью POSIX. Символические ссылки могут ссылаться на любой файл в системе. Они могут также ссылаться нз каталоги. Это упрощает перемещение каталогов с места на место, когда на старом месте остается символическая ссылка, указывающая на новое положение. При обработке имени файла система отмечает символические ссылки и осуществляет требуемые действия в файле или каталоге, который указан. Символические ссылки создаются с помощью опции -s команды In: $ /bin/pwd - /* Где мы находимся */ /d/home/arnold /* В другой файловой системе */ $ In -s /tmp/message ./hello /* Создать символическую ссылку */ $ cat hello /* Использовать ее */ Hi, how ya doin' ?
134 Глава 5. Каталоги и служебные данные файлов $ Is -1 hello /* Отобразить информацию о ней */ lrwxrwxrwx 1 arnold devel 12 May 4 16:41 hello -> /tmp/message Файл, на который указывает ссылка, необязательно должен существовать. Система обнаруживает это во время исполнения й действует соответствующим образом: $ rm /tmp/message /* Удалить указываемый файл */ $ cat ./hello /* Попытка использования через символическую ссылку*/ cat: ./hello: No such file or directory •> $ echo hi again > hello /* Создать новое содержание файла */ $ Is -1 /tmp/message /* Показать информацию об указываемом файле */ -rw-r--r-- 1 arnold devel 9 May. 4 16:45 /tmp/message $ cat /tmp/message /* ...и содержание */ hi again Символические ссылки создаются с помощью системного вызова symlink (): #include <unistd.h> /* POSIX */ int symlink(const char *oldpath, const char *newpath); Аргумент oldpath содержит указываемый файл или каталог, a newpath является именем создаваемой символической ссылки. При успехе возвращается 0, а при ошибке (-1); возможные значения errno см. в справочной странице для symlink{2). У символических ссылок есть свои недостатки: Они занимают лишнее дисковое пространство, требуя отдельного индекса и блока данных. Прямые ссылки занимают лишь элемент каталога. • Они добавляют лишние накладные расходы. Ядро должно больше работать для разрешения имени пути, содержащего символические ссылки. Они могут создать «циклы». Рассмотрите следующее: $ rm -f a b /* Убедиться, что 'а' и Чо' не существуют */ $ In -s a b /* Создать ссылку старого файла ха' на новый ХЬ' */ $ In -s b a /* Создать ссылку старого файла Чэ' на новый va' */ $ cat a /* Что случилось? */ cat: a: Too many levels of symbolic links Ядро должно быть способно определить такой случай и выдать сообщение об ошибке. Они легко обрываются. Если переместить указываемый файл в другое место или переименовать его, символическая ссылка больше не действительна. С прямой ссылкой такого случиться не может. 5.2. Создание и удаление каталогов Создание и удаление каталогов просто. Двумя системными вызовами, что неудивительно, являются mkdir () и rmdir () соответственно: #include <sys/types.h> /*-POSIX */ #include <sys/stat.h> int mkdir(const char *pathname, mode_t mode);
5.2. Создание и удаление каталогов 135 #include <unistd.h> /* POSIX */ int rmdir(const char *pathname);• Оба возвращают 0 при успехе и (-1) при ошибке, с соответствующим еггпо. Аргумент mode для mkdir () представляет права доступа, которые должны быть использованы для каталога. Он полностью идентичен аргументам mode для creat () и open (), обсуждавшимся в разделе 4.6 «Создание файлов». Обе функции обрабатывают V и '..' в создаваемом или удаляемом каталоге. Перед удалением каталог должен быть пуст; если это не так, еггпо устанавливается в ENOTEMPTY. (В данном случае, «пуст» означает, что каталог содержит только V и '..'.) Новым каталогам, как и всем файлам, присваивается идентификационный номер группы. К сожалению, его работа запутана. Мы отложим обсуждение до раздела 11.5.1 «Группа по умолчанию для новых файлов и каталогов». Обе функции работают на одном уровне каталога зараз. Если /somedir существует, a /somedir/subl нет, 'mkdir('7somedir/subl/sub2")' завершится неудачей. Каждый компонент в длинном пути должен создаваться отдельно (в соответствии с опцией -р mkdir, см. mkdir(l)). Также, если pathname завершается символом V, на некоторых системах mkdir () и rmdir() потерпят неудачу, а на других нет. Следующая программа, ch05-trymkdir .с, демонстрирует оба аспекта. 1 /* ch05-trymkdir .с Демонстрирует поведение mkdir (¦). 2 Любезность Nelson H.F. Beebe. */ 3 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <errno.h> 7 8 #if !defined(EXIT_SUCCESS) 9 #define EXIT_SUCCESS-0 10 #endif 11 12 void do_test(const char *path) 13 { 14 int retcode; 15 16 errno = 0; 17 retcode = mkdir(path/ 0755); 18 printf("mkdir(\"%s\H) returns %d: errno = %d [%s]\n", 19 path, retcode, errno, strerror(errno)); 20 } 21 22 int main(void) 23 {
136 Глава 5. Каталоги и служебные данные файлов 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 do_test("/tmp/tl/12/13/14"! do_test("/tmp/tl/t2/t3"); do_test("/tmp/tl/t2"); do_test("/tmp/tl"); do_test(' do_test(' do__test (' do_test(' do_test(' do_test(' do_test(' do_test(' /*Попытка создания в подкаталоге*/ /* Создать подкаталоги */ /tmp/ul"); /tmp/ul/u2"); /tmp/ul/u2/u3"); /tmp/ul/u2/u3/u4") /tmp/vl/"); /* Как обрабатывается завершающий V? */ /tmp/vl/v2/"); /tmp/vl/v2/v3/"); /tmp/vl/v2/v3/v4/' return (EXIT_SUCCESS); } errno Вот результаты для GNU/Linux: $ ch05-trymkdir mkdir("/tmp/tl/t2/t3/t4") returns tory] mkdir("/tmp/tl/t2/t3") returns -1 tory] mkdir("/tmp/tl/t2") returns mkdir("/tmp/tl") returns 0: mkdir("/tmp/ul") returns 0: mkdir("/tmp/ul/u2") returns mkdir("/tmp/ul/u2/u3") returns 0: errno = 0 mkdir("/tmp/ul/u2/u3/u4") returns 0: errno errno = 2 [No such file or direc- 2 [No such file or direc- -1: errno = 2 '[No such file or directory] errno = 0 [Success] errno = 0 [Success] 0: errno = 0 [Success] [Success] 0 [Success] mkdir("/tmp/vl/") returns 0: errno = 0 [Success] mkdir("/tmp/vl/v2/") returns 0: errno = 0 [Success] mkdir("/tmp/vl/v2/v3/") returns 0: errno = 0 [Success] mkdir("/tmp/vl/v2/v3/v4/ ") returns 0: errno = 0 [Success] Обратите внимание, как GNU/Linux принимает завершающий слеш. Не все системы так делают. 5.3. Чтение каталогов В оригинальных системах Unix чтение содержимого каталогов было просто. Программа открывала каталог с помощью open () и непосредственно читала двоичные структуры struct direct, no 16 байтов за раз. Следующий фрагмент кода из программы V7 rmdir , строки 60-74. Он показывает проверку на пустоту каталога. См. /usr/src/cmd/rmdir . с в дистрибутиве V7. - Примеч. автора.
5.3. Чтение каталогов 137 60 if((fd = open(name,0)) < 0) { 61 fprintf(stderr, "rmdir: %s unreadable\n", name); 62 ++Errors; 63 return; 64 } 65 while(read(fd, (char *)&dir, sizeof dir) == sizeof dir) { 66 if(dir.d_ino == 0) continue; 67 if(!strcmp(dir.d_name, ".") || !strcmp(dir.d_name, "..")) 68 continue; 69 fprintf(stderr, "rmdir: %s not empty\n", name); 7 0 ++Errors; 71 close(fd); 72 return; 73 } 74 close(fd); В строке 60 каталог открывается для чтения (второй аргумент равен 0, что означает 0_RDONLY). В строке 65 читается struct direct. В строке 66 проверяется, не является ли элемент каталога пустым, т. е. с номером индекса 0. Строки 67 и 68 проверяют на наличие '.' и '..'. По достижении строки 69 мы знаем, что было встречено какое-то другое имя файла, следовательно, этот каталог не пустой. (Тест4! strcmp (si, s2)' является более короткой формой'strcmp (si, s2) == О'; т. е. проверкой совпадения строк. Стоит заметить, что мы рассматриваем '! strcmp (si, s2)' как плохой стиль. Как сказал однажды Генри Спенсер (Henry Spencer), «strcmp () это не boolean!».) Когда 4.2 BSD представило новый формат файловой системы, который допускал длинные имена файлов и обеспечивал лучшую производительность, были также представлены несколько новых функций для абстрагирования чтения каталогов. Этот набор функций можно использовать независимо от того, какова лежащая в основе файловая система и как организованы каталоги. .Основная ее часть стандартизована POSIX, а программы, использующие ее, переносимы между системами GNU/Linux и Unix. 5.3.1. Базовое чтение каталогов Элементы каталогов представлены struct dirent (не то же самое, что V7 struct direct!): struct dirent { ino_t d_ino; /* расширение XSI см. текст *¦/ char d__name [ . . . ] ; /* О размере этого массива см. в тексте */ };
138 Глава 5. Каталоги и служебные данные файлов Для переносимости POSIX указывает лишь поле d__name, которое является завершающимся нулем массивом байтов, представляющим часть элемента каталога с именем файла. Размер d__name стандартом не указывается, кроме того, что там перед завершающим нулем может быть не более NAME__MAX байтов. (NAME_MAX определен в <limits .h>.) Расширение XSI POSIX предусматривает поле номера индекса d__ino. На практике, поскольку имена файлов могут быть различной длины, a NAME_MAX обычно довольно велико (подобно 255), struct dirent содержит дополнительные члены, которые помогают вести на диске учет элементов каталогов с переменными длинами. Эти дополнительные члены не существенны для обычного кода. Следующие функции предоставляют интерфейс чтения каталогов: #include <sys/types.h> /* POSIX */ #include <dirent.h> DIR *opendir(const char *name); /* Открыть каталог для чтения */ struct dirent *readdir(DIR *dir); /* Вернуть struct dirent за раз */ int closedir(DIR *dir); /* Закрыть открытый каталог */ void rewinddir(DIR *dirp); /* Вернуться в начало каталога */ Тип DIR является аналогом типа FILE в <stdio.h>. Это непрозрачный тип, что означает, что код приложения не должен знать, что находится внутри него; его содержимое предназначено для использования другими процедурами каталогов. Если opendir () возвращает NULL, именованный каталог не может быть открыт для чтения, a errno содержит код ошибки. Открыв переменную DIR*, можно использовать ее для получения указателя на struct dirent, представляющего следующий элемент каталога, readdir () возвращает NULL, если достигнут конец каталога4 или произошла ошибка. Наконец, closedir() является аналогичной функции fcloseO в <stdio.h>; она закрывает открытую переменную DIR*. Чтобы начать с начала каталога, можно использовать функцию rewinddir (). Имея в распоряжении (или по крайней мере в библиотеке С) эти функции, мы можем написать небольшую программу catdir, которая «отображает» содержимое каталога. Такая программа представлена в ch05-catdir.с: 1 /* ch05-catdir .с, - Демонстрация opendir () , readdir () , closedirO. */ 2 3 #include <stdio.h> /* для printf() и т.д. */ 4 #include <errno.h> /* для errno */ 5 #include <sys/types.h> /* для системных типов */ 6 #include <dirent.h> /* для функций каталога */ 7 8 char *myname; 9 int process(char *dir); To есть прочитаны все элементы каталога. - Примеч. науч. ред.
5.3. Чтение каталогов 139 ю 11 /* main перечисление аргументов каталога */ 12 13 int main(int argc, char **argv) 14 { . 15 int i; 16 . int errs = 0; ¦ ' 17 18 myname = argv[0]; 19 20 if (argc ^=1) 21 errs = process("."); /* по умолчанию текущий каталог */ 22 else 2 3 for (i = 1; i < argc; i++) 24 errs += process(argv[i]); 25 2 6 return (errs != 0); 27 } Эта программа вполне подобна ch04-cat. с (см. раздел 4.2 «Представление базовой структуры программы»); функция main () почти идентична. Главное различие в том, что по умолчанию используется текущий каталог, если нет аргументов (строки 20-21). 29 /* 30 * process сделать что-то с каталогом, в данном случае, '31 * вывести пары индекс/имя в стандартный вывод. 32 * Возвращает 0, если все ОК, иначе 1. 33 */ 34 ' . . 35 int 36 process(char *dir) 37 { 3 8 DIR *dp; 39 struct dirent *ent; 40 41 if ((dp = opendir(dir)) == NULL) { 42 fprintf(stderr, "%s: %s: cannot open for reading: %s\n", 43 myname, dir, strerror(errno)); 44 return ,1 ; 45 } 46 47 errno = 0; i 48 while ((ent = readdir(dp)) != NULL) 49 printf("%81d %s\n", ent->d_ino, ent->d_name); 50 51 if (errno != 0) {
140 Глава 5. Каталоги и служебные данные файлов 52 fprintf(stderr, "%s: %s: reading directory entries: %s\n", 53 myname, dir, strerror(errno)); 54 return 1; 55 } 56 57 if (closedir(dp) != 0) { 58 fprintf(stderr, "%s: %s: closedir: ^s\n", 59 myname, dir, strerror(errno)); 60 return 1; 61 }' 62 63 return 0; 64 } Функция process () делает всю работу и большую часть кода проверки ошибок. Основой функции являются строки 48 и 49: while (Cent = readdir(dp)) != NULL) printf("%81d %s\n"/ ent->d_ino, ent->d_name); Этот цикл читает элементы каталога, по одной за раз, до тех пор, пока readdir () не возвратит NULL. Тело цикла отображает для каждого элемента номер индекса и имя файла. Вот что происходит при запуске программы: $ ch05-catdir /* По умолчанию текущий каталог */ 639063 . 639062 . . 639064proposal.txt 639012 lightsabers.url 688470 code 638976 progex.texi 639305 texinfo.tex 639007 15-processes.texi 639011 00-preface.texi 639020 18-tty.texi 638980 Makefile 639239 19-il8n.texi Вывод никаким образом не сортируется; он представляет линейное содержимое каталога. (Как сортировать содержимое каталога мы опишем в разделе 6.2 «Функции сортировки и поиска».) 5.3.1.1. Анализ переносимости Есть несколько соображений по переносимости. Во-первых, не следует предполагать, что двумя первыми элементами, возвращаемыми readdir (), всегда будут '.' и '. . \ Многие файловые системы используют организацию каталогов, которые отличаются от
5.3. Чтение каталогов 141 Первоначального дизайна Unix, и *.' и '. .' могут быть в середине каталога или даже вовсе не присутствовать5. Во-вторых, стандарт POSIX ничего не говорит о возможных значениях d_inf о. Он говорит, что возвращенные структуры представляют элементы каталогов для файлов; это предполагает, что readdirO не возвращает пустые элементы, поэтому реализация GNU/Linux readdir () не беспокоится с возвратом элементов, когда vd_ino == 0'; она переходит к.следующему действительному элементу. Поэтому по крайней мере на системах GNU/Linux и Unix маловероятно, что d_ino когда-нибудь будет равен нулю. Однако, лучше по возможности вообще избегать использования этого поля. Наконец, некоторые системы используют d_fileno вместо d_ino в struct dirent. Знайте об этом, когда нужно перенести на такие системы код, читающий каталоги. Косвенные системные вызовы «Не пробуйте это дома, дети!» - М-р Wizard - Многие системные вызовы, такие, как open (), read () и write (), предназначены для вызова непосредственно из кода пользователя: другими словами, из кода, который пишете вы как разработчик GNU/Linux. Однако, другие системные вызовы существуют лишь для того, чтобы дать возможность реализовать стандартные библиотечные функции более высокого уровня, и никогда не должны вызываться непосредственно. Одним из таких системных вызовов является GNU/Linux get dents (); он читает несколько элементов каталога в буфер, предоставленный вызывающим - в данном случае, кодом реализации readdir (). Затем код readdir () возвращает действительные элементы каталога, по одному за раз, пополняя при необходимости буфер. Эти системные вызовы только-для-библиотечного-использования можно отличить от вызовов для-йспользования-пользователем по их представлению в странице справки. Например, из getdents{2)\ имя getdents - получить элементы каталога ОПИСАНИЕ #include <unistd.h> Mnclude <linux/ types .h> #include <linux/dirent.h> ' #include <linux/unistd.h> В системах GNU/Linux могут монтироваться файловые системы многих операционных систем, не относящихся к Unix. Во многих коммерческих системах Unix также можно смонтировать файловые системы MS-DOS. В таких случаях предположения относительно файловых систем Unix неприменимы. - Примеч. автора.
142 Глава 5. Каталоги и служебные данные файлов _syscall3(int, getdents, uint, fd, struct dirent*, dirp, uint, count); int getdents(unsigned int fd, struct dirent *dirp, unsigned int count); Любой системный вызов, использующий макрос _syscallX(), не должен вызываться кодом приложения. (Дополнительную информацию об этих вызовах можно найти в справочной странице для intro{2)\ вам следует прочесть эту справочную страницу, если вы этого еще не сделали.) В случае getdents () на многих других системах Unix есть сходный системный вызов; иногда с тем же именем, иногда с другим. Поэтому попытка использования этих вызовов привела бы в любом случае лишь к большому беспорядку v с переносимостью; гораздо лучше во всех случаях использовать readdir (), интерфейс которого хорошо определен, стандартизован и переносим. 5.3.1.2. Элементы каталогов Linux и BSD Хотя мы только что сказали, что вам следует использовать лишь члены d_ino и d__name структуры struct dirent, стоит знать о члене d_type в struct dirent BSD и Linux. Это значение unsigned char, в котором хранится тип файла, имя которого находится в элементе каталога: struct dirent { ino_t d_ino; /* Как ранее */ char d_name[...]; /* Как ранее */. unsigned char d_type; /* Linux и современная BSD */ } ; d_type может принимать любые значения, описанные в табл. 5.1. Знание типа файла просто путем чтения элемента каталога очень удобно; это может сэкономить на возможно дорогом системном вызове stat (). (Вызов stat() вскоре будет описан в разделе 5.4.2 «Получение информации о файле».) Таблица 5.1. Значения для d_type Имя Значение DT_BLK Файл блочного устройства. DT_CHR Файл символьного устройства. DT_DIR Каталог. DT_FIFO FIFO или именованный канал. DT LNK Символическая ссылка.
5.4. Получение информации о файлах 143 Таблица 5.1. Значения для d_type (Продолжение) Имя Значение DT__REG Обычный файл. DT_SOCK Сокет. DT_UNKNOWN Неизвестный тип файла. DT_WHT Нет элемента (только системы BSD). 5.3.2. Функции размещения каталогов BSD Иногда полезно отметить текущее положение в каталоге для того, чтобы иметь возможность позже к нему вернуться. Например, вы пишете код, обходящий дерево каталога, и хотите рекурсивно входить в каждый подкаталог, когда его проходите. (Как отличить файлы от каталогов обсуждается в следующем разделе.) По этой причине первоначальный интерфейс BSD включал две дополнительные процедуры: #include <dirent.h> XSI /* Предупреждение: POSIX XSI использует для обеих функций long, а не off_t */ off_t telldir(DIR *dir); /* Вернуть текущее положение */ void seekdir(DIR *dir, off_t offset); /* Переместиться в данное положение */ Эти процедуры подобны функциям f tell () и f seek () и <stdio. h>. Они возвращают текущее положение в каталоге и устанавливают текущее положение в ранее полученное значение соответственно. Эти процедуры включены в часть XSI стандарта POSIX, поскольку они имеют смысл лишь для каталогов, которые реализованы с линейным хранением элементов каталога. Помимо предположений, сделанных относительно лежащей в основе структуры каталога, эти процедуры рискованнее использовать, чем простые процедуры чтения каталога. Это связано с тем, что содержание каталога может изменяться динамически: когда файлы добавляются или удаляются из каталога, операционная система приводит в порядок содержание каталога. Поскольку элементы каталога имеют различный размер, может оказаться, что сохраненное ранее абсолютное смещение больше не представляет начало элемента каталога! Поэтому мы не рекомендуем вам использовать эти функции, если вам они действительно не нужны6. 5.4. Получение информации о файлах Чтение каталога для получения имен файлов лишь половина дела. Получив имя файла, нужно знать, как получить остальную информацию, связанную с файлом, такую, как тип файла, права доступа к нему, владельца и т. д. Стоит внимательно подумать прежде чем использовать эти функции. - Примеч. науч. ред.
144 Глава 5. Каталоги и служебные данные файлов 5.4.1. Типы файлов Linux Linux (и Unix) поддерживает следующие различные типы файлов: Обычные файлы как предполагает имя, данный тип используется для данных, исполняемых программ и всего прочего, что вам может понравиться. В листинге 'Is -1' они обозначаются в виде первого символа '-' поля прав доступа (режима). Каталоги Специальные файлы для связывания имен файлов с индексами. В листинге 'Is -1' они обозначаются первым символом d поля прав доступа. Символические ссылки Как описано ранее в главе. В листинге 'Is -1' обозначаются первым символом 1 (буква «эль», не цифра 1) поля прав доступа. Устройства Файлы, представляющие как физические аппаратные устройства, так и программные псевдоустройства. Есть две разновидности: Блочные устройства Устройства, ввод/вывод которых осуществляется порциями некоторого фиксированного размера физической записи, такие, как дисковые и ленточные приводы. Доступ к таким устройствам осуществляется через буферный кэш ядра. В листинге 'Is -1' они обозначаются первым символом b поля прав доступа. Символьные устройства Известны также как непосредственные (raw) устройства. Первоначально символьными устройствами были те, в которых ввод/вывод осуществлялся по несколько байтов за раз, как в терминалах. Однако, символьное устройство используется также для непосредственного ввода/вывода на блочные устройства, такие, как ленты и диски, минуя буферный кэш7. В листинге 'Is -1' они отображаются первым символом с поля прав доступа. Именованные каналы (namedpipes) Известны также файлы FIFO («first-in first-out» - «первым вошел, первым обслужен»). Эти специальные файлы действуют подобно конвейерам (pipes); данные, записанные в них одной программой, могут быть прочитаны другой; данные не записываются на диск и не считываются с диска. FIFO создаются с помощью команды mkf if о; они обсуждаются в разделе 9.3.2 «FIFO». В листинге 'Is -1' они отображаются первым символом р поля прав доступа. Linux использует блочные устройства исключительно для дисков. Другие системы используют оба типа. - Примеч. автора.
5.4. Получение информации о файлах 145 Сокеты о Сходные по назначению с именованными каналами , они управляются системными вызовами межпроцессных взаимодействий (IPC) сокетов, и мы не будем в данной книге иметь с ними дело в других отношениях. В листинге 'Is -1' они отображаются первым символом s поля прав доступа. 5.4.2. Получение информации о файле Три системных вызова возвращают информацию о файлах: #include <sys/types.h> • /* POSIX */ #include <sys/stat.h> #include <unistd.h> int stat(const char *file_name, struct stat *buf); int fstat(int filedes, struct stat *buf); int lstat(const char *file_name, struct stat *buf); Функция stat () получает имя файла с путем и возвращает сведения о данном файле. Она следует по символическим ссылкам; т. е. при применении к символической ссылке stat () возвращает информацию об указываемом файле, а не о самой ссылке. В тех случаях, когда вам нужно знать, является ли файл символической ссылкой, используйте вместо нее функцию lstat (); последняя не следует по символическим ссылкам. Функция f stat () получает сведения об уже открытом файле. Это особенно полезно для дескрипторов файлов 0, 1 и 2 (стандартных ввода, вывода и ошибки), которые уже открыты при запуске процесса. Однако, она может использоваться с любым открытым файлом. (Дескриптор открытого файла никогда не будет относиться к символической ссылке; убедитесь, что понимаете, почему.) Значение, переданное в качестве второго параметра, должно быть адресом struct stat, объявленной в <sys/stat .h>. Как в случае с struct dirent, struct stat содержит по крайней мере следующие члены: struct stat { dev_t st__dev; /* устройство */ ino_t st_ino; /* индекс •*/ mode_t st_mode; /* тип и защита */ nlink_t st_nlink; /* число прямых (hard) ссылок */ uid_t st_uid; /* ID владельца */ gid_t st_gid; /* ID группы */ dev_t st_rdev; /* тип устройства (блочное или символьное) */ off_t st_size; /* общий размер в байтах */ blksize_t st_blksize; /* размер блока для ввода/вывода файл, с-мы */ blkcnt__t st_blocks; /* число выделенных блоков */ Именованные каналы и сокеты были разработаны независимо группами Unix System V и BSD соответственно. Когда системы Unix вновь сошлись, обе разновидности файлов стали доступными универсально. - Примеч. автора.
146 Глава 5. Каталоги и служебные данные файлов time_t st_atime; /* время последнего доступа */ time_t st_mtime; /* время последнего изменения */ time_t st_ctime; /* время последнего изменения индекса */ }; (Размещение на разных системах может быть разное.) Эта структура использует ряд определенных через typedef типов. Хотя они все (обычно) целые типы, использование специально определенных типов позволяет использовать для них различные размеры на разных системах. Это сохраняет переносимость кода пользователя, который их использует. Вот более полное описание каждого поля. st_dev Устройство для монтируемой файловой системы. У каждой монтируемой файловой системы уникальное значение st_dev. st__ino Номер индекса файла в пределах файловой системы. Пара (st_dev, st_ino) уникально идентифицирует файл. st_mode Тип файла и права доступа к нему, закодированные в одном поле. Вскоре мы рассмотрим, как извлечь эту информацию. st_nlink Число прямых ссылок на файл (счетчик ссылок): Может равняться нулю, если файл был удален после открытия. st_uid UID файла (номер владельца). st_gid GID файла (номер группы). st_rdev Тип устройства, если файл является блочным или символьным устройством, s t__rdev содержит закодированную информацию об устройстве. Вскоре мы увидим, как извлечь эту информацию. Это поле не имеет смысла, если файл не является блочным или символьным устройством. st_size Логический размер файла. Как упоминалось в разделе 4.5 «Произвольный доступ: перемещение внутри файла», файл может содержать в себе дыры, в этом случае размер может не отражать истинного значения занимаемого им места. st_blksize «Размер блока» файла. Представляет предпочтительный размер блока данных для ввода/вывода данных в или из файла. Почти всегда превышает размер физического сектора диска. У более старых систем Unix нет этого поля (или поля
5.4. Получение информации о файлах 147 st_blocks) в struct stat. Для файловых систем Linux ext2 и ext3 это значение составляет 4096. st_blocks Число «блоков», используемых файлом. В Linux это значение представлено в единицах 512-байтных блоков. На других системах размер блока может быть различным; проверьте свою локальную страницу справки для stat{2). (Это число происходит от константы DEV_BSIZE в <sys/param.h>. Эта константа не стандартизована, но довольно широко используется в системах Unix.) Число блоков может быть больше, чем 'st_size / 512'; кроме блоков данных, файловая система может использовать дополнительные блоки для хранения размещений блоков данных. Это особенно необходимо для больших файлов. st_atime Время доступа к файлу; т. е. когда в последний раз читались данные файла. st_mtime Время модификации файла; т. е. когда в последний раз данные файла записывались или урезались. st__ctime Время изменения индекса файла. Указывает время последнего изменения служебных данных файла, таких, как права доступа или владелец. ^ЗАМЕЧАНИЕ. Поле st_ctime не является «временем создания»! В системе Linux \или Unix нет такой вещи. Часть более ранней документации называла поле \st__ctime временем создания. Это была вводящая в заблуждение попытка упро- I стить представление служебных данных файла. Тип time__t, использованный для полей st_atime, st_mtime и st_ctime, представляет дату и время. Эти относящиеся ко времени значения иногда называют временны- ми метками (timestamps). Обсуждение того, как использовать значение time_t, отложено до раздела 6.1 «Время и даты». Подобным же образом типы uid_t и gid__t представляют номера владельца и группы, которые обсуждаются в разделе 6.3 «Имена пользователя и группы». Большинство других типов не представляют широкого интереса. 5.4.3. Только Linux: указание файлового времени повышенной точности Ядра Linux 2.6 и более поздние предоставляют в struct stat три дополнительных поля. Они предусматривают точность файлового времени до наносекунд: st_atime_nsec Наносекундная компонента времени доступа к файлу. st_mtime__nsec Наносекундная компонента времени изменения файла. st_ctime_nsec Наносекундная компонента времени изменения служебных данных файла.
148 Глава 5. Каталоги и служебные данные файлов Некоторые другие системы также предоставляют такие поля с повышенной точностью времени, но имена соответствующих членов структуры struct stat не стандартизованы, что затрудняет написание переносимого кода, использующего эти времена. (Связайные с этим расширенные системные вызовы см. в разделе 14.3.2 «Файловое-время в микросекундах: utimes () ».) 5.4.4. Определение типа файла Вспомните, что в поле st_m6de закодированы как тип файла, так и права доступа к нему. <sys/stat.h> определяет ряд макросов, которые определяют тип файла. В частности, эти макросы возвращают true или false при использовании с полем st_mode. У каждого описанного ранее типа файла есть свой макрос. Предположим, выполняется следующий код: struct stat stbuf; char filename [PATH_MAX] ; /* PATH_MAX из <limits.h> */ /* ... поместить имя файла в filename ... */ if (stat(filename,'& stbuf) < 0) { /* обработать ошибку */ } Когда система заполнила stbuf, можно вызывать следующие макросы, причем в качестве аргумента передается stbuf . st_mode: S_ISREG (stbuf .st__mode) Возвращает true, если filename является обычным файлом. S_ISDIR(stbuf.st_mode) Возвращает true, если filename является каталогом. S_ISCHR(stbuf.st_mode) Возвращает true, если filename является символьным устройством. Устройства вскоре будут обсуждены более подробно. S_ISBLK(stbuf.st_mode) Возвращает true, если filename является блочным устройством. S_ISFIFO(stbuf.st_mode) Возвращает true, если filename является FIFO. S_ISLNK(stbuf.st_mode) Возвращает true, если filename является символической ссылкой. (Это может никогда не вернуть true, если вместо lstatf) использовались stat () или f stat ().) S_ISSOCK(stbuf.st_mode) Возвращает true, если filename является сокетом.
5.4. Получение информации о файлах 149 I ЗАМЕЧАНИЕ. В GNU/Linux эти макросы возвращают 1 для true и 0 для false. Одна- \ко, на других системах возможно, что они будут возвращать для true вместо 1 произвольное неотрицательное число. (POSIX определяет лишь ненулевое значение I в противоположность нулевому) Поэтому всегда следует использовать эти макро- I сы как автономные тесты вместо проверки возвращаемого значения: I/* Корректное использование */ lif (S_ISREG(stbuf.st_mode)) .... I/* Не корректное использование */ * |if (S_ISREG(stbuf.st_mode) == 1) . . . Наряду с макросами <sys/ s tat. h> предоставляет два набора битовых масок. Один набор для проверки прав доступа, а другой - для проверки типа файла. Мы видели маски прав доступа в разделе 4.6 «Создание файлов», когда обсуждали тип mode_t и значения для open () и creat (). Битовые маски, их числовые значения для GNU/Linux и смысл приведены в табл. 5.2. Некоторые из этих масок служат цели изолирования различных наборов битов, закодированных в поле st_mode: S_IFMT представляет биты 12-15, которыми закодирован^ различные типы файлов. • S_IRWXU представляет биты 6-8, являющиеся правами доступа владельца (на чтение, запись, исполнение для User). • S__IRWXG представляет биты 3-5, являющиеся правами доступа группы (на чтение, запись, исполнение для Group). • S_IRWXO представляет биты 0-2, являющиеся правами доступа для «остальных» (на чтение, запись, исполнение для Other). Биты прав доступа и типа файла графически изображены на рис. 5.3. Таблица 5.2. Битовые маски POSIX для типов файлЪв и прав доступа в <sys/ s tat. h> Маска S_IFMT S_IFSOCK S_IFLNK S_IFREG S_IFBLK S_IFDIR S_IFCHR S_IFIFO S_ISUID S_ISGID Значение 0170000 0140000 0120000 0100000 0060000 0040000 0020000 0010000 0004000 0002000 Комментарий Маска для битовых полей типа файла. Сокет. Символическая ссылка. Обычный файл. Блочное устройство. Каталог. Символьное устройство. FIFO. Бит setuid. BHTsetgid.
150 Глава 5. Каталоги и служебные данные файлов Таблица 5.2. Битовые маски POSIX для типов файлов и прав доступа в <sys/ stat. h> (Продолжение) Маска Значение Комментарий S. s_ s_ s_ s_ s_ s. s_ s_ s_ s. s_ s_ _ISVTX _IRWXU _IRUSR _IWUSR .IXUSR _IRWXG _IRGRP .IWGRP _IXGRP _IRWXO _IROTH _IWOTH _IXOTH 0001000 0000700 0000400 0000200 0000100 0000070 0000040 0000020 0000010 0000007 0000004 0000002 0000001 «Липкий» (sticky) бит. Маска для прав доступа владельца. Доступ на чтение для владельца. Доступ на запись для владельца. Доступ на исполнение для владельца. Маска для прав доступа группы. Доступ на чтение для группы. Доступ на запись для гругшы. Доступ на исполнение для группы. Маска для прав доступа остальных. Доступ на чтение для остальных. Доступ на запись для остальных. Доступ на исполнение для остальных. Маски типов файлов стандартизованы главным образом для совместимости со старым кодом; они не должны использоваться непосредственно, поскольку такой код менее читаем, чем соответствующие макросы. Сл учается, что макрос реализован с использованием масок довольно логично, но это не подходит для кода уровня пользователя. 15 14 13 12 I I I Тип файла 11 10 7 I I Г Г Г Г I SUID Г SGID Г SVTXf Владелец r/w/x II Г Г $№%Ш тштштт J- ШОЖШ: 5 4 3 Группа r/w/x 2 1.0 Другие r/w/x Рис. 5.3. Биты прав доступа и типа файлов Стандарт POSIX явным образом констатирует^ что в будущем не будут стандартизированы новые битовые маски и что тесты для любых дополнительных разновидностей типов файлов, которые могут быть добавлены, будут доступны лишь в виде макросов S_ISxxx ().
5.4. Получение информации о файлах 151 5.4.4.1. Сведения обустройстве Стандарт POSIX не определяет значение типа dev_t, поскольку предполагалось его использование на не-Unix системах также, как на Unix-системах. Однако стоит знать, что находится в dev_t. Когда истинно S_ISBLK(sbuf . st_mode) или S_ISCHR(sbuf . st_mode), сведения об устройстве находятся в поле sbuf . st_rdev.,B противном случае это поле не содержит никакой полезной информации. Традиционно файлы устройств Unix кодируют старший и младший номера устройства в значении dev_t. По старшему номеру различают тип устройства, такой, как «дисковый привод» или «ленточный привод». Старшие номера различают также разные типы устройств, такие, как диск SCSI в противоположность диску IDE. Младшие номера различают устройства данного типа, например, первый диск или второй. Вы можете увидеть эти значения с помощью ' 1 s -1': $ Is -1 /dev/hda /dev/hda? /* Показать номера для первого жесткого диска */ brw-rw 1 root disk 3, 0 Aug 31 2002 /dev/hda brw-rw 1 root disk 3, 1 Aug 31 2002 /dev/hdal brw-rw 1 root disk 3,2 Aug 31 2002 /dev/hda2 brw-rw- 1 root disk 3, 3 Aug 31 2002 /dev/hda3 brw-rw 1 root disk 3, 4 Aug 31 2002 /dev/hda4 brw-rw 1 root disk 3, 5 Aug 31 2002 /dev/hda5 brw-rw 1 root disk 3, 6 Aug 31 2002 /dev/hda6 brw-rw 1 root disk 3, 7 Aug 31 2002 /dev/hda7 brw-rw 1 root disk 3, 8 Aug 31 2002 /dev/hda8 brw-rw 1 root disk 3, 9 Aug 31 2002 /dev/hda9 $ Is -1 /dev/null /* Показать сведения также для /dev/null */ crw-rw-rw- 1 root root 1, 3 Aug 31 2002 /dev/null Вместо размера файла Is отображает старший и младший номера. В случае жесткого диска /dev/hda представляет диск в целом, /dev/hdal, /dev/hda2 и т. д. представляют разделы внутри диска. У них у всех общий старший номер устройства C), но различные младшие номера устройств. Обратите внимание, что дисковые устройства являются блочными устройствами, тогда как /dev/null является символьным устройством. Блочные и символьные устройства являются отдельными сущностями; даже если символьное устройство и блочное устройство имеют один и тот же старший номер устройства, они необязательно связаны. Старший и младший номера устройства можно извлечь из значения dev__t с помощью функций ma j or () и minor (), определенных в <sys / sysmacros . h>: #include <sys/types.h> /* Обычный */ #include <sys/sysmacros.h> int major(dev_t dev); /* Старший номер устройства */ int minor(dev_t dev); /* Младший номер устройства */ dev_t makedev(int major, int minor); /* Создать значение dev_t */ (Некоторые системы реализуют их в виде макросов.)
152 Глава 5. Каталоги и служебные данные файлов Функция makedevO идет другим путем; она принимает отдельные значения старшего и младшего номеров и кодирует их в значении dev_t. В других отношениях ее использование выходит за рамки данной книги; патологически любопытные должны посмотреть mknodB). Следующая программа, ch05-devnum.c, показывает, как использовать системный вызов stat (), макросы проверки типа файла и, наконец, макросы ma j or () и minor (). /* ch05-devnum.с Демонстрация stat(), major()/ minor(). */ #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/sysmacros.h> int main(int argc, char **argv) { struct stat sbuf; char *devtype; if (argc != 2) { fprintf(stderr, "usage: %s path\n", argv[0]); exit(l); } if (stat(argv[l], & sbuf) < 0) { fprintf(stderr, "%s: stat: %s\n", argv[l], strerror(errno)); exitA); } if (S_ISCHR(sbuf.st_mode)) devtype = "char"; else if (S_ISBLK(sbuf.st_mode)) devtype = "block"; else { fprintf(stderr, "%s is not a block or character device\n", argv[1]); exitA); } printf("%s: major: %d, minor: %d\n", devtype, major(sbuf.st_rdev), minor(sbuf.st_rdev)); exit@); } Вот что происходит при запуске программы: $ ch05-devnum /tmp /* Попробовать не устройство */ /tmp is not a block or character device $ ch05-devnum /dev/null /* Символьное устройство */ char: major: 1, minor: 3
5.4. Получение информации о файлах 153 $ ch05-devnum /dev/hda2 /* Блочное устройство */ block: major: 3, minor: 2 К счастью, вывод согласуется с выводом Is, давая нам уверенность , что мы в самом деле написали правильный код. Воспроизведение вывода Is замечательно и хорошо, но действительно ли это полезно? Ответ - да. Любое приложение, работающее с иерархиями файлов, должно быть способно различать различные типы файлов. Подумайте об архиваторе, таком как tar или cpio. Было бы пагубно, если бы такая программа рассматривала файл дискового устройства как обычный файл, пытаясь прочесть его и сохранить его содержимое в архиве! Или подумайте о find, которая может выполнять произвольные действия, основываясь на типе и других атрибутах файлов, с которыми она сталкивается, (find является сложной программой; посмотрите find(l), если вы с ней не знакомы.) Или даже нечто простое, как пакет, оценивающий свободное дисковое пространство, тоже должно отличать обычные файлы от всего остального. 5.4.4.2. Возвращаясь к Wca t В разделе 4.4.4 «Пример: Unix cat» мы обещали вернуться к программе V7 cat, чтобы посмотреть, как она использует системный вызов stat (.). Первая группа строк, использовавшая ее, была такой: 31 fstat(fileno(stdout), fcstatb); 32 statb.st_mode &= S_IFMT; 33 if (statb.st_mode!=S_IFCHR && statb.st_mode!=S__IFBLK) { 34 dev = statb.st_dev; 3 5 ino = statb.st_ino; 36 } Этот код теперь должен иметь смысл. В строке 31 вызывается fstat () для стандартного вывода, чтобы заполнить структуру statb. Строка 32 отбрасывает всю информацию в statb. st__mode за исключением типа файла, используя логическое AND с маской S_IFMT. Строка 33 проверяет, что используемый для стандартного вывода файл не является файлом устройства. В таком случае программа сохраняет номера устройства и индекса в dev и ino. Эти значения затем проверяются для каждого входного файла в строках 50-56: 50 fstat(fileno(fi), &statb); 51 if (statb. st_dev==dev && statb. st__ino==ino) { 52 fprintf(stderr, "cat: input %s is output\n", 53 fflg?"-": *argv); 54 fclose(fi); 55 continue; 56 } Технический термин warm fuzzy. - Примеч. автора.
154 Глава 5. Каталоги и служебные данные файлов Если значения st_dev и st__ino входного файла совпадают с соответствующими значениями выходного файла, cat выдает сообщение и продолжает со следующего файла, указанного в командной строке. Проверка сделана безусловно, хотя dev и ino устанавливаются, лишь если вывод не является файлом устройства. Это срабатывает нормально из-за того, как эти переменные объявлены: int dev, ino = -1; Поскольку ino инициализирован значением (-1), ни один действительный номер индекса не будет ему соответствовать10. То, что dev не инициализирован так, является небрежным, но не представляет проблемы, поскольку тест в строке 51 требует, чтобы были равными значения как устройства, так и индекса. (Хороший компилятор выдаст предупреждение, что dev используется без инициализации: 'gcc -Wall' сделает это.) Обратите также внимание, что ни один вызов f stat () не проверяется на ошибки. Это также небрежность, хотя не такая большая; маловероятно, что f stat () завершится неудачей с действительным дескриптором файла. Проверка того, что входной файл не равен выходному файлу, осуществляется лишь для файлов, не являющихся устройствами. Это дает возможность использовать cat для копирования ввода из файлов устройств в самих себя, как в случае с терминалами: $ tty /* Вывести имя устройства текущего терминала */ /dev/pts/3 $ cat /dev/pts/3 > /dev/pts/3 /* Копировать ввод от клавиатуры на экран */ this is a line of text /* Набираемое в строке */ this is a line of text /* cat это повторяет */ 5.4.5. Работа с символическими ссылками В общем, символические ссылки ведут себя подобно прямым ссылкам; файловые операции, такие, как ореп() и stat О, применяются к указываемому файлу вместо самой символической ссылки. Однако, бывают моменты, когда в самом деле необходимо работать с символической ссылкой вместо файла, на которую она указывает. По этой причине существует системный вызов lstat (). Он действует точно также, как stat (), но если проверяемый файл окажется символической ссылкой, возвращаемые сведения относятся к символической ссылке, а не к указываемому файлу. А именно: S_ISLNK (sbuf . s t__mode) будет true. sbuf . st_size содержит число байтов в имени указываемого файла. Мы уже видели, что системный вызов symlink() создает символическую ссылку. Но если дана существующая символическая ссылка, как можно получить имя файла, на Это утверждение было верно для V7; на современных системах больше нет таких гарантий. - Примеч. автора.
5.4. Получение информации о файлах 155 которую она указывает? (Очевидно, Is может получить это имя; поэтйму мы должны быть способны это сделать.) Открывание ссылки с помощью open () для чтения ее с использованием read (¦) не будет работать; open () следует по ссылке на указываемый файл. Таким образом, символические ссылки сделали необходимым дополнительный системный вызов, который называется readlink (): #include <unistd.h> /* POSIX */ int readlink(const char *path, char *buf, size_t bufsiz); readlink () помещает содержимое символической ссылки, на имя которой указывает path, в буфер, на который указывает buf. Копируется не более bufsiz символов. Возвращаемое значение равно числу символов, помещенных в buf, либо -1, если возникла ошибка, readlink () не вставляет завершающий нулевой байт. Обратите внимание, что если буфер, переданный readlink (), слишком маленький, информация будет потеряна; полное имя указываемого файла будет недоступно. Чтобы использовать readlink () должным образом, вы должны делать следующее: 1. Используйте Is tat (), чтобы убедиться, что это символическая ссылка. 2. Убедитесь, что ваш буфер для содержимого символической ссылки составляет по крайней мере 'sbuf . st_size + 1' байтов;'+ Г нужно для завершающего нулевого байта, чтобы сделать буфер годной к употреблению строкой С. 3. Вызовите readlinkO. He мешает проверить, что возвращенное значение равно sbuf.st_size. 4. Добавьте 4 \ 0' к байту после содержимого ссылки, чтобы превратить его в строку С. Код для всего этого мог бы выглядеть примерно так: /* Проверка ошибок для краткости опущена */ int count; char linkfile[PATH_MAX] , realf ile [PATH_MAX] ; /* PATH_MAX в <limits.h> */ strut stat sbuf; /* ...поместить в linkfile путь к нужной символической ссылке... */ lstat(linkfile, & sbuf); /* Получить сведения от stat */ if (! S_ISLNK(sbuf.st_mode)) /* Проверить, что это ссылка */ /* не символическая ссылка, обработать это */ if (sbuf.st_size + 1 > РАТН_МАХ) /* Проверить размер буфера */ /* обработать проблемы с размером буфера */ count = readlink(linkfile, realfile, PATH_MAX); /* Прочесть ссылку */ if (count != sbuf.st_size) /* происходит что-то странное, обработать это */ realfile[count] = '\0'; /* Составить строку С */ Данный пример для простоты представления использует буферы фиксированного размера. Реальный код мог бы использовать для выделения буфера нужного размера malloc О, поскольку массивы фиксированного размера могли бы оказаться,слишком маленькими. Файл lib/xreadlink.c в GNU Coreutils делает именно это. Он читает
156 Глава 5. Каталоги и служебные данные файлов содержимое символической ссылки в память, выделенную malloc (). Мы покажем здесь саму функцию; большая часть файла представляет собой стереотипные определения. Номера строк относятся к началу файла: 55 /* Вызвать readlink для получения значения ссылки FILENAME. 56 Вернуть указатель на завершенную NUL строку в выделенной malloc памяти. 57 При ошибке readlink вернуть NULL (использовать errno для диагноза) . 58 При ошибке realloc или если значение ссылки больше ST.ZE._MAX, 59 выдать диагностику и выйти. */ 60 61 char * 62 xreadlink (char const * filename) 63 { 64 /* Начальный размер буфера для ссылки. Степень 2 обнаруживает 65 арифметическое переполнение раньше, но не рекомендуется. */ 66 size_t buf_size = 12 8; 67 68 while A) 69. { 70 char *buffer = xmalloc (buf_size); 71 ssize_t link_length = readlink (filename, buffer, buf_size); 72 7 3 if (link_length < 0) 74 { 7 5 int saved_errno = errno; 76 free (buffer); 77 errno = saved_errno; 7 8 return NULL; 79 } 80 81 if ( (size__t) link__length < buf_size) 82 { 83 buffer[link_length] = 0; 84 return buffer; 85 } 86 87 free (buffer); 88 buf_size *= 2; 89 if (SSIZEJYLAX < buf_size | | (SIZE_MAX / 2 < SSIZE_MAX && buf_size == 0) ) 90 xalloc_die (); ' 91 } 92 } Тело функции состоит из бесконечного цикла (строки 68-91), разрываемого в строке 84, которая возвращает выделенный буфер. Цикл начинается выделением первоначального
5.4. Получение информации о файлах 157 буфера (строка 70) и чтения ссылки (строка 71). Строки 73-79 обрабатывают случай ошибки, сохраняя и восстанавливая errno таким образом, что она может корректно использоваться вызывающим кодом. Строки 81-85 обрабатывают случай «успеха», при котором размер содержимого ссылки меньше размера буфера. В этом случае добавляется завершающий ноль (строка 83), а затем буфер возвращается, прерывая бесконечный цикл. Это гарантирует, что в буфер помещено все содержимое ссылки, поскольку у readlink () нет возможности сообщить о «недостаточном размере буфера». Строки 87-88 освобождают буфер и удваивают размер буфера для следующей попытки в начале цикла. Строки 89-90 обрабатывают случай, при котором размер ссылки слишком велик: buf_size больше, чем SSIZE_MAX, или SSIZE_MAX больше, чем значение, которое может быть представлено в знаковом целом того же размера, который использовался для хранения SIZE_MAX, и buf_size обернулся в ноль. (Это маловероятные условия, но странные вещи все же случаются.) Если одно из этих условий верно, программа завершается с сообщением об ошибке. В противном случае функция возвращается в начало цикла, чтобы сделать еще одну попытку выделить буфер и прочесть ссылку. Некоторое дополнительное разъяснение: условие 'SIZE_MAX / 2 < SSIZE_MAX' верно лишь на системах, в которых 4SIZE_MAX < 2 * SSIZE_MAX'; мы не знаем таких, но лишь на таких системах buf_size может обернуться в ноль. Поскольку на практике это условие не может быть истинным, компилятор может оптимизировать все выражение, включив следующую проверку 'buf_size == 0'. После прочтения этого кода вы можете спросить: «Почему не использовать Is tat () для получения размера символической ссылки, не выделить буфер нужного размера с помощью malloc (), и все?» На это есть несколько причин11. Is tat () является системным вызовом - лучше избежать накладных расходов по его вызову, поскольку содержимое большинства символических ссылок поместится в первоначальный размер буфера в 128. • Вызов Is tat () создает условие состязания: ссылка может измениться между исполнением lstat () и readlink (), в любом случае вынуждая повторение. Некоторые системы не заполняют должным образом член st_size для символической ссылки. (Печально, но верно.) Сходным образом, как мы увидим в разделе 8.4.2 «Получение текущего каталога: getcwd () », Linux в /ргос предоставляет специальные символические ссылки, у которых st_size равен нулю, но для которых readlink () возвращает действительное содержимое. Наконец, буфер не слишком большой, xreadlink () использует free () и malloc () с большим размером вместо reallocO, чтобы избежать бесполезного копирования, которое делает reallocO. (Поэтому комментарий в строке 58 устарел, поскольку realloc () не используется; это исправлено в версии Coreutils после 5.0.) Спасибо Джиму Мейерингу (Jim Meyering) за объяснение проблем. - Примеч. автора.
158 Глава 5. Каталоги и служебные данные файлов 5.5. Смена владельца, прав доступа и времени изменения Несколько других системных вызовов дают вам возможность изменять другие относящиеся к файлу сведения: в частности, владельца и группу файла, права доступа к файлу и времена доступа и изменения файла. 5.5.1. Смена владельца файла: chown (), fchown () и Ichown () Владелец и группа файла изменяются с помощью трех сходных системных вызовов: #include <sys/types.h> /* POSIX */ #include <unistd.h> int chown(const char *path, uid_t owner, gid_t group); int fchown (int ?d, uid_t owner, gid__t group); int Ichown (const char *path, uid__t owner, gid_t group); chown () работает с аргументом имени файла, fchown () работает с открытым файлом, a Ichown () работает с символической ссылкой вместо файла, на который эта ссылка указывает. Во всех других отношениях эти три вызова работают идентично, возвращая О в случае успеха и -1 при ошибке. Стоит заметить, что один системный вызов изменяет как владельца, так и группу файла. Чтобы изменить лишь владельца или лишь группу, передайте (-1) в качестве того идентификационного номера, который должен остаться без изменений. Хотя вы могли бы подумать, что можно передать соответствующее значение из полученного заранее struct stat для файла или файлового дескриптора, этот метод больше подвержен ошибкам. Возникает условие состязания: между вызовами stat () и chown () владелец или группа могут измениться. Вы могли бы поинтересоваться: «Зачем нужно изменять владельца символической ссылки? Права доступа и владение ей не имеют значения». Но что случится, если пользователь уходит, а все его файлы все еще нужны? Необходима возможность изменения владельца всех файлов этого лица на кого-то еще, включая символические ссылки. Системы GNU/Linux обычно не позволяют рядовым пользователям (не root) изменять владельца («отдавать») своих файлов. Смена группы на одну из групп пользователя, конечно, разрешена. Ограничение в смене владельцев идет от BSD систем, у которых тоже есть этот запрет. Главная причина в том, что разрешение пользователям отдавать файлы может нарушить дисковый учет. Рассмотрите такой сценарий: $ mkdir mywork /* Создать каталог */ $ chmod go-rwx mywork /* Установить права доступа drwx -- */ $ cd mywork /* Перейти в него */ $ myprogram > large_data_file /* Создать большой файл */ $ chmod ugo+rw large_data_file /* Установить доступ -rw-rw-rw- */ $ chown otherguy large_data_file /* Передать файл otherguy */
5.5. Смена владельца, прав доступа и времени изменения 159 В этом примере large_data_f ile теперь принадлежит пользователю other guy. Первоначальный пользователь может продолжать читать и записывать файл из-за его прав доступа. Но дисковое пространство, которое он занимает, будет записано на счет otherguy. Однако, поскольку он находится в каталоге, который принадлежит первому пользователю и к которому otherguy не может получить доступ, otherguy не имеет возможности удалить файл. Некоторые системы System V разрешают пользователям передавать свои файлы. (При смене владельца соответствующие биты файлов setuid и setgid сбрасываются.) Это может быть особенной проблемой, когда файлы извлекаются из архива .tar или .cpio; извлеченные файлы имеют UID и GID, закодированный в архиве. На таких системах программы tar и cpio имеют опции, предотвращающие это, но важно знать, что поведение chown () действительно отличается на разных системах. В разделе 6.3 «Имена пользователя и группы» мы увидим, как соотносить имена пользователя и группы с соответствующими числовыми значениями. 5.5.2. Изменение прав доступа: chmod () и f chmod () Изменение прав доступа осуществляется с помощью одного из двух системных вызовов, chmod() HfchmodO: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> int chmod(const char *path, mode__t mode); int fchmod(int fildes, mode_t mode); chmod () работает с аргументом имени файла, a f chmod () работает с открытым файлом. (В POSIX нет вызова 1 chmod (), поскольку система игнорирует установки прав доступа для символических ссылок. Хотя на некоторых системах такой вызов действительно есть.) Как и для большинства других системных вызовов, они возвращают 0 в случае успеха и -1 при ошибке. Права доступа к файлу может изменить лишь владелец файла или root. Значение mode создается таким же образом, как для open () и creat (), как обсуждалось в разделе 4.6 «Создание файлов». См. также табл. 5.2, в которой перечислены константы прав доступа. Система не допустит установки бита setgid (S__ISGID), если группа файла не совпадает с ID действующей группы процесса или с одной из его дополнительных групп. (Мы пока не обсуждали подробно эти проблемы; см. раздел 11.1.1 «Реальные и действующие ID».) Разумеется, эта проверка не относится к root или коду, выполняющемуся как root. 5.5.3. Изменение временных отметок: utime () Структура struct stat содержит три поля типа time__t: st__at ime Время последнего доступа к файлу (чтение). st__mtime Время последнего изменения файла (запись). st_ctime Время последнего изменения индекса файла (например, переименования).
160 Глава 5. Каталоги и служебные данные файлов Значение time_t представляет время в «секундах с начала эпохи». Эпоха является Началом Времени для компьютерных систем. GNU/Linux и Unix используют в качестве начала Эпохи полночь 1 января 1970 г. по универсальному скоординированному времени (UTCI2. Системы Microsoft Windows используют в качестве начала Эпохи полночь 1 января 1980 г. (очевидно, местное время). Значения time_t иногда называют временными отметками (timestamps). В разделе 6.1 «Время и даты» мы рассмотрим, как получаются эти данные и как они используются. Пока достаточно знать, чем является значение time_t и то, что оно представляет секунды с начала Эпохи. Системный вызов utime () позволяет изменять отметки времени доступа к файлу и его изменения: #include <sys/types.h> /* POSIX */ #include <utime.h> int utime(const char ^filename, struct utimbuf *buf); Структура utimbuf выглядит следующим образом: struct utimbuf { time_t actime; /* время доступа */ time_t modtime; /* время изменения */ }/ При успешном вызове возвращается 0; в противном случае возвращается -1. Если buf равен NULL, система устанавливает время доступа и время изменения равным текущему времени. Чтобы изменить только одну временную отметку, используйте оригинальное значение из struct stat. Например: /* Для краткости проверка ошибок опущена */ struct stat sbuf; struct utimbuf ut; time_t now; time(& now); /* Получить текущее время дня, см. след. главу */ stat("/some/file", & sbuf); /* Заполнить sbuf */ ut.асtime = sbuf.st_atime; /* Время доступа без изменений */ ut.modtime = now - B4 * 60 * 60); /* Установить modtime на 24 часа позже */ utime("/some/file", & ut); /* Установить значения */ Вы можете спросить себя: «Почему может понадобиться кому-нибудь изменять времена доступа и изменения файла?» Хороший вопрос. 2 UTC представляет собой независимое от языка сокращение для Coordinated Universal Time (универсальное скоординированное время). Старый код (а иногда и люди постарше) называют это Гринвичским временем (Greenwich Mean Time, GMT), которое является временем в Гринвиче, Великобритания. Когда стали широко использоваться часовые пояса, в качестве точки отсчета, относительно которого все остальные часовые пояса отсчитывались либо вперед, либо назад, был выбран Гринвич. - Примеч. автора.
5.5. Смена владельца, прав доступа и времени изменения 161 Чтобы на него ответить, рассмотрите случай программы, срздающей дублирующие архивы, такой, как tar или cpio. Эти программы должны прочесть содержание файла, чтобы заархивировать его. Чтение файла, конечно, изменяет время доступа к файлу. Однако, этот файл, возможно, не читался человеком в течение 10 лет. Некто, набрав 4Is -lu\ что отображает время доступа (вместо времени изменения по умолчанию), увидел бы, что последний раз данный файл просматривали 10 лет назад. Поэтому программа архивации должна сохранить оригинальные значения времени доступа и изменения, прочесть файл для архивации, а затем восстановить первоначальное время с помо- uibK)utime(). Аналогичным образом, рассмотрите случай архивирующей программы, восстанавливающей файл из архива. В архиве хранятся первоначальные значения времени доступа и изменения. Однако, когда файл извлечен из архива во вновь созданную копию на диске, новый файл имеет текущие дату и время для значений времени доступа и изменения. Однако полезнее, когда вновь созданный файл выглядит, как если бы он имел тот же возраст, что и оригинальный файл в архиве. Поэтому архиватор должен иметь возможность устанавливать значения времени доступа и изменения в соответствии со значениями в архиве. 13АМЕЧАНИЕ.В новом коде вы можете захотеть использовать вызов и times () (обратите внимание на s в имени), который описан далее в книге, в разделе 14.3.2 «Файловое время в микросекундах: и times ()». 5.5.3.1. Подделка utime (file, null) Некоторые более старые системы не устанавливают значения времени доступа и изменения равным текущему времени, когда второй аргумент utime () равен NULL. Однако код более высокого уровня (такой, как GNU touch) проще, если он может полагаться на один стандартизованный интерфейс. Поэтому библиотека GNU Coreutils содержит замещающую функцию для utime (), которая обрабатывает, этот случай, которую потом может вызвать код более высокого уровня. Это отражает принцип проектирования «выбор лучшего интерфейса для работы», который мы описали в разделе 1.5 «Возвращаясь к переносимости». Замещающая функция находится в файле lib/utime.c в дистрибутиве Coreutils. Следующий код является версией из Coreutils 5.0. Номера строк относятся к началу файла: 24 #include <sys/types.h> 25 2 6 #ifdef HAVE_UTIME_H 27 # include <utime.h> 2 8 #endif 29 30 #inelude "full-write.h" 31 #include "safe-read.h" 32 6-159
162 Глава 5. Каталоги и служебные данные файлов 33 /* Некоторые системы (даже имеющие <utime.h>) нигде не объявляют 3 4 эту структуру. */ 35 #ifndef HAVE_STRUCT_UTIMBUF 36 struct utimbuf 37 { 3 8 long actime; 3 9 long modtime; 40 }; 41 #endif 42 43 /* Эмулировать utime (file, NULL) для систем (подобных 4.3BSD), 44 которые не устанавливают в этом случае текущее время для времени 45 доступа и изменения file. Вернуть 0, если успешно, -1 если нет. */ 46 47 static int 48 utime_null (const char *file) 49 { 50 #if HAVE_UTIMES_NULL 51 return utimes (file, 0); 52 #else 53 int fd; 54 char c; 55 int status = 0; 56 struct stat sb; 57 58 fd = open (file, 0_RDWR); 59 if (fd < 0 60 || fstat (fd, &sb) < 0 61 j j safe_read (fd, &c, sizeof c) == SAFE_READ_ERROR 62 II lseek (fd, (off_t) 0, SEEK_SET) < 0 63 || full_write (fd, &c, sizeof c) != sizeof с 64 /* Можно сделать - это необходимо на SunOS4.1.3 с некоторой комбинацией 65 заплат, но та система не использует этот код: у нее есть utimes. 66 || fsync (fd) < 0 67 */ 68 || (st.st_size == 0 && ftruncate (fd, st.st_size) < 0) 69 || close (fd) < 0) 70 status = -1; 71 return status; 72 #endif 73 } 74 75 int % 76 rpl_utime (const char *file, const struct utimbuf *times) 77 { 78 if (times)
5.5. Смена владельца, прав доступа и времени изменения 163 79 return utime (file, times); 80 81 return utime__null (file); i 82 } Строки 33-41 определяют структуру struct utimbuf; как сказано в комментарии, некоторые системы не объявляют эту структуру. Работу осуществляет функция utime_null (). Используется системный вызов utimesO, если он доступен, (utimes () является сходным, но более развитым системным вызовом, который рассматривается в разделе 14.3.2 «Файловое время в микросекундах: utimes () ». Он допускает также в качестве второго аргумента NULL, что означает использование текущего времени.) В случае, когда время должно обновляться вручную, код осуществляет обновление, прочитав сначала из файла байт, а затем записав его обратно. (Первоначальный touch Unix работал таким способом.) Операции следующие: 1. Открыть файл, строка 58. 2. Вызвать для файла stat (), строка 60. 3. Прочесть один байт, строка 61. Для наших целей safe_read() действует подобно read (); это объясняется в разделе 10.4.4 «Повторно запускаемые системные вызовы»). 4. Переместиться обратно на начало файла с помощью lseek (), строка 62. Это сделано для записи только что прочитанного байта обратно поверх себя. 5. Записать байт обратно, строка 63. f ull_write () действует подобно write (); это также рассматривается в разделе 10.4.4 «Повторно запускаемые системные вызовы»). 6. Если файл имеет нулевой размер, использовать ftruncateO для установки его размера в ноль (строка 68). Это не изменяет файл, но имеет побочный эффект обновления времени доступа и изменения. (ftruncateO была описана в разделе 4.8 «Установка длины файла».) 7. Закрыть файл, строка 69. Все эти шаги осуществляются в одной длинной последовательной цепи проверок внутри if. Проверки сделаны так, что если любое сравнение неверно, utime_null () возвращает -1, как обычный системный вызов, errno автоматически устанавливается системой для использования кодом более высокого уровня. Функция rpl__utime() (строки 75-82) является «заместителем utime ()». Если второй аргумент не равен NULL, она вызывает настоящую utime (). В противном случае она вызывает utime_null (). 5.5.4. Использование f chown () и f chmod () для обеспечения безопасности В исходных системах Unix были только системные вызовы chown () и chmod (). Однако, на сильно загруженных системах эти системные вызовы попадают в условия состязания, посредством чего злоумышленник может организовать замещение другим файлом файла, у которого изменяется владелец или права доступа. а*
164 Глава 5. Каталоги и служебные данные файлов Однако, после открытия файла условие состязания больше не представляет проблему. Программа может использовать stat() с именем файла для получения информации о файле. Если получены сведения, которые ожидались, после открытия файла f stat () может проверить, что файл тот же самый (сравнив поля st_dev и st_ino структур struct stat «до» и «после»). Когда программа знает, что файлы те же самые, владение или права доступа могут быть изменены с помощью f chown () или f chmod (). Эти системные вызовы, также как IchownO, сравнительно недавние13; в старых системах Unix их не было, хотя в современных совместимых с POSIX системах они есть. Соответствующих функций futime () или lutime () нет. В случае futime () это (очевидно) потому, что временные отметки не являются критическими для безопасности системы в том же отношении, что для владения и прав доступа, lutime () отсутствует потому, что временные отметки неуместны для символических ссылок. 5.6. Резюме • Иерархия файлов и каталогов, как она видится пользователю, является одним логическим деревом, корень которого находится в /. Оно составлено из одного или более разделов, каждый из которых содержит файловую систему. Внутри файловой системы в индексах хранятся данные о файлах (метаданные), включая размещение блоков данных. • Каталоги осуществляют связь между именами файлов и индексами. Концептуально содержимое каталога, которое является просто последовательностью пар (индекс, имя). Каждый элемент каталога для файла называется (прямой) ссылкой, а файлы могут иметь множество ссылок. Прямые ссылки, поскольку они работают лишь по номеру индекса, все должны находиться в одной файловой системе. Символические ссылки являются указателями на файлы или каталоги и работают на основе имени файла, а не номера индекса, поэтому их использование не ограничено одной и той же файловой системой. • Прямые ссылки создаются с помощью link(), символические ссылки создаются с помощью symlink (), ссылки удаляются с помощью unlink (), а переименовываются файлы (с возможным перемещением в другой каталог) с помощью rename (). Блоки данных файла не освобождаются до тех пор, пока счетчик ссылок не достигнет нуля и не закроется последний открытый дескриптор файла. • Каталоги создаются с помощью mkdir (), а удаляются с помощью rmdir (); перед удалением каталог должен быть пустым (не оставлено ничего, кроме '.' и '..'). GNU/Linux версия функции ISO С remove () вызывает соответствующие функции unlink () или rmdir (). • Каталоги обрабатываются с помощью функций opendirO, readdirO, rewinddir () и closedir (). struct dirent содержит номер индекса и имя файла. Максимально переносимый код использует в члене d_name только имя файла. f chown () и f chmod () были введены в 4.2 BSD, но не включались в System V до выпуска 4. - Примеч. автора.
Упражнения 165 Функции BSD telldir () и seekdir () для сохранения и восстановления текущего положения в каталоге широко доступны, но не полностью переносимы, как другие функции работы с каталогами. • Вспомогательные данные получаются с помощью семейства системных вызовов stat (); структура struct stat содержит всю информацию о файле за исключением имени файла. (В самом деле, поскольку у файла может быть множество имен или он может совсем не иметь ссылок, невозможно сделать имя доступным.) • Макрос S_ISxxx() в <sys/stat.h> дает возможность определить тип файла. Функции major () и minor () из <sys/sysmacros . h> дают возможность расшифровки значений dev_t, представляющих блочные и символьные устройства. Символические ссылки можно проверить, использовав lstat(), а поле st_size структуры struct stat для символической ссылки возвращает число байтов, необходимых для размещения имени указываемого файла. Содержимое символической ссылки читают с помощью readlink(). Нужно позаботиться о том, чтобы размер буфера был правильным и чтобы завершить полученное имя файла нулевым байтом, чтобы можно было его использовать в качестве строки С. Несколько разнообразных системных вызовов обновляют другие данные: семейство chownO используется для смены владельца и группы, процедуры chmod () для прав доступа к файлу, a ut ime () для изменения значений времени доступа и изменения файла. Упражнения 1. Напишите программу 'const char *fmt__mode (mode__t mode)'. Ввод представляет собой значение mode__t, полученное из поля st__mode структуры struct stat; т. е. оно содержит как биты прав доступа, так и типа файла. Вывод должен представлять строку в 10 символов, идентичную первому полю вывода 'Is -1'. Другими словами, первый символ обозначает тип файла, а остальные девять - права доступа. Когда установлены биты S_ISUID и S_IXUSR, используйте s вместо х; если установлен лишь бит 1_Д SUID, используйте S. То же относится к битам S_ISGID и S__IXGRP. Если установлены оба бита S_ISVTX и S_IXOTH, используйте t; для одного S_ISVTX используйте Т. Для простоты можете использовать статический (static) буфер, содержимое которого перезаписывается при каждом вызове процедуры. 2. Доработайте ch05-catdir. с, чтобы она вызывала stat () для каждого найденного имени файла. Затем выведите номер индекса, результат вызова f mt__mode (), число ссылок и имя файла. 3. Доработайте ch05-catdir.с так, что если файл является символической ссылкой, программа будет также выводить имя указываемого файла. 4. Добавьте такую опцию, что если имя файла является именем подкаталога, программа рекурсивно входит в него и отображает сведения о файлах (и каталогах) этого подкаталога. Необходим лишь один уровень рекурсии.
166 Глава 5. Каталоги и служебные данные файлов 5. Если вы не работаете на системе GNU/Linux, запустите ch05-trymkdir (см. раздел 5.2 «Создание и удаление каталогов») на своей системе и сравните результаты с приведенными нами. 6. Напишите программу mkdir. Посмотрите свою локальную справочную страницу для mkdir(l) и реализуйте все ее опции. 7. В корневом каталоге, /, как номер устройства, так и номер индекса для '.'и'..' совпадают. Используя эту информацию, напишите программу pwd. Вначале программа должна найти имя текущего каталога, прочитав содержимое родительского каталога. Затем она должна продолжить собирать сведения о иерархии файловой системы, пока не достигнет корневого каталога. Отображение имени каталога в обратном порядке, от текущего каталога до корневого, легко. Как будет справляться ваша версия pwd с выводом имени каталога правильным образом, от корневого каталога вниз? 8. Если вы написали pwd, использовав рекурсию, напишите ее снова, использовав итерацию. Если вы использовали итерацию, напишите ее с использованием рекурсии. Что лучше? (Подсказка: рассмотрите очень глубоко вложенные деревья каталогов.) 9. Тщательно исследуйте функцию rpl_utime() (см. раздел 5.5.3.1 «Подделка utime (file, NULL)»), Какой ресурс не восстанавливается, если одна из проверок в середине if не выполняется? (Спасибо Джеффу Колье (Geoff Collyer).) 10. (Трудное) Прочтите страницу справки для chmod{\). Напишите код для анализа аргумента символических опций, который позволяет добавлять, удалять и устанавливать права доступа на основе владельца, группы, других и «всех». Когда вы решите, что это работает, напишите свою собственную версию chmod, которая применяет назначенные права доступа к каждому файлу или каталогу, указанному в командной строке. Какую функцию вы использовали, chmod () - или open () и f chmod () - и почему?
Глава 6 Общие библиотечные интерфейсы - часть 1 В этой главе: • 6.1. Времена и даты .167 • 6.2. Функции сортировки и поиска 181 • 6.3. Имена пользователей и групп 195 • 6.4. Терминалы: isatty () 201 • 6.5. Рекомендуемая литература 202 • 6.6. Резюме 203 • Упражнения 203 Г$ главе 5 «Каталоги и служебные данные файлов» мы видели, что непосредственное чтение каталога возвращает имена файлов в том порядке, в каком они хранятся в каталоге. Мы также видели, что struct stat содержит всю информацию о файле за исключением его имени. Однако, некоторые компоненты этой структуры не могут использоваться непосредственно; они являются просто числовыми значениями. В данной главе представлена оставшаяся часть API, необходимая для полного использования значений компонентов struct stat. Мы по порядку рассматриваем следующие темы: значения time__t для представления времени и функций форматирования времени; функции сортировки и поиска (для сортировки имен файлов или других данных); типы uid_t и gid_t для представления пользователей, групп и функций, которые сопоставляют их с соответствующими именами пользователей и групп; и наконец, функцию для проверки того, что дескриптор файла представляет терминал. 6.1. Времена и даты Значения времени хранятся в типе, который известен как time__t. Стандарт ISO С гарантирует, что это числовой тип, но во всем остальном никакие указывает, чем именно он является (целым или с плавающей точкой), как и не указывает степень точности хранящихся в нем значений. На системах GNU/Linux и Unix значения time_t представляют «секунды с начала Эпохи». Эпоха представляет собой начало записываемого времени, которое относится к полночи 1 января 1970 г. по UTC. На большинстве систем time_t является long int С. Для 32-разрядных систем это означает, что time_t переполнится 19 января 2038 г. К тому
168 Глава 6. Общие библиотечные интерфейсы - часть 1 времени, мы надеемся, тип time_t будет переопределен как по меньшей мере 64-разрядное значение. Для получения текущего времени, вычисления разницы между двумя значениями t ime_t, преобразования значений time_t в более удобное представление и форматирования обоих представлений в виде символьных строк существуют различные функции. Вдобавок, представление даты и времени можно преобразовать обратно в time_t, доступна также ограниченная информация по часовым поясам. Отдельный набор функций предоставляет доступ к текущему времени с разрешением, большим чем одна секунда. Функции работают с предоставлением двух различных значений: времени в виде секунд с начала Эпохи и числа микросекунд в текущей секунде. Эти функции описаны далее в разделе 14.3.1 «Время в микросекундах: gettimeof day () ». 6.1.1. Получение текущего времени: time () и dif f time () Системный вызов time () получает текущие дату и время; dif f time () вычисляет разницу между двумя значениями time_t: #include <time.h> /* ISO С */ time_t time (time__t *t) ; double difftime(time_t timel, time_t timeO) ; time () возвращает текущее время. Если параметр t не равен NULL, переменная, на которую указывает t, также заполняется значением текущего времени. Функция возвращает (time_t) (-1), если была ошибка, устанавливая errno. Хотя ISO С не указывает, чем является значение time_t, POSIX определяет, что оно представляет время в секундах. Поэтому это предположение является обычным и переносимым. Например, чтобы посмотреть, что значение времени представляет отметку в прошлом шесть месяцев назад или позже, можно использовать код, подобный этому: /* Для краткости проверка ошибок опущена */ time_t now, then, some_time; time(& now); /* Получить текущее время */ then = now - FL ¦* 31 * 24 * 60 * 60); /* Примерно 6 месяцев назад */ /* ...установить какое-нибудь время, например, через stat()... */ if (some_time < then) /* более 6 месяцев назад */ else /* менее б месяцев назад */ Однако, поскольку переносимый код может потребоваться запустить на не-POSIX системах, существует функция difftimeO для вычисления разницы между двумя значениями времени. Тот же самый тест с использованием dif f time () можно было бы написать таким способом: time_t now, some_value; const double six_months = 6 . 0' * 31 * 24 * 60 * 60; time(& now); /* Получить текущее время */
6.1. Временам даты 169 /* ...установить какое-нибудь время, например, через stat()...*/ if (diff time (now, some_time) >= six__months) /* более 6 месяцев назад */ else /* менее б месяцев назад */ Возвращаемым типом difftimeO является double, поскольку time__t может также содержать доли секунд. На системах POSIX он всегда представляет целые секунды. В обоих предыдущих примерах обратите внимание на использование типизированных констант, чтобы форсировать выполнение вычислений с нужным математическим типом: 6L в первом случае для целых long, б . О во втором случае для чисел с плавающей точкой. 6.1.2. Разложение времени: gmtime () и localtime () На практике форма представления даты и времени в виде «секунд с начала эпохи» не является очень удобной, кроме очень простых сравнений. Самостоятельное вычисление компонентов времени, таких, как месяц, день, год и т. д., подвержено ошибкам, поскольку необходимо принять во внимание местный часовой пояс (возможно, с учетом перехода на летнее время), правильно вычислить високосные годы и пр. К счастью, две стандартные процедуры делают за вас эту работу: #include <time.h> /* ISO С */ struct tm *gmtime(const time_t *timep); struct.tm *localtime (const time_t *timep); gmtime () возвращает указатель на struct tm, которая представляет время UTC. localtime () возвращает указатель на struct tm, представляющий местное время; т. е. в расчет берутся текущий часовой пояс и переход на летнее время. На самом деле это «время для настенных часов», дата и время, которые были бы отображены на настенных или ручных часах. (Как это работает, обсуждается далее в разделе 6.1.5 «Получение сведений о часовом поясе».) Обе функции возвращают указатель на struct tm, которая выглядит следующим образом: struct tm { int int int int int int int int int tm_sec; tm_min; tm_hour; tm_mday; tm_mon; tm_year; tm_wday; tm_yday; tm_isdst; /* /* /* /* /* /* /* /* /* секунды */ минуты */ часы */ день месяца */ месяц */ год */ день недели */ день в году */ летнее время */ };
170 Глава 6. Общие библиотечные интерфейсы - часть 1 struct tm называютразложенным временем (broken-down time), поскольку значение time_t «разложено» на свои составные части. Составные части, их диапазоны и значения показаны в табл. 6.1. Таблица 6.1. Поля структуры tm Член Диапазон Значение tm_sec tm_min tm_hour tm_mday tm_mon tm_year tm__wday tm_yday tm_isdst 0-60 0-59 0-23 1-31 0-11 0-/V 0-6 0-365 <0,0,>0 Секунда минуты. Секунда 60 предусматривает пропущенные (leap) секунды. (В С89 был диапазон 0-61.) Минута часа. Час дня. День месяца. Месяц года. Год, начиная с 1900 г. День недели, воскресенье = 0. День года, 1 января = 0. Флаг летнего времени. Стандарт ISO С представляет большинство этих значений как «х после у». Например, tm_sec является числом «секунд после минуты», tm__mon «месяцев после января», tm_wday «дней недели после воскресенья» и т. д. Это помогает понять, почему все значения начинаются с 0. (Единственным исключением, достаточно логичным, является tm_mday, день месяца, имеющий диапазон 1-31.) Конечно, отсчет их с нуля также практичен; поскольку массивы С отсчитываются с нуля, использование этих значений в качестве индексов тривиально: static const char *const days[] = { /* Массив имен дней */ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", }; time_t now; struct tin *curtime; time(& now); /* Получить текущее время */ curtime = gmtime(& now); /* Разложить его */ printf("Day of the week: %s\n", days[curtime->tm_wday]); /* Проиндексировать и вывести */ Как gmtime (), так и localtime () возвращают указатель на struct tm. Указатель указывает на static struct tm, содержащуюся в каждой процедуре, и похоже, что эти структуры struct tm переписываются каждый раз, когда вызываются процедуры. Поэтому хорошая мысль сделать копию возвращенной struct. Возвращаясь к предыдущему примеру:
6.1. Времена и даты 171 static const char *const days[] = { /* Как ранее */ }; time_t now; ' . ' ' struct tm curtime; /* Структура, а не указатель */ time(& now); /* Получить текущее время */ curtime = *gmtime(& now); /* Разложить его и скопировать данные*/ printf("Day of the week: %s\n", days[curtime.tm_wday]); /* Проиндексировать и напечатать, использовать . , а не ->¦ */ Поле tm_isdst указывает, действует ли в настоящий момент летнее время (DST). Значение 0 означает, что DST не действует, положительное значение означает, что действует, а отрицательное значение - что информация о DST недоступна. (Стандарт С намеренно неконкретен, указывая лишь нулевое, положительное и отрицательное значения; это дает возможность большей свободы при реализации.) 6.1.3. Форматирование даты и времени Примеры в предыдущем разделе показали, как поля в struct tm могли бы быть использованы в качестве индексов символьных строк для вывода информативных значений даты и времени. Хотя можно было бы написать собственный код, использующий такие массивы для форматирования даты и времени, стандартные процедуры облегчают работу. 6. 7. 3.1. Простое форматирование времени: asctime () net ime () Две первые стандартные процедуры, перечисленные ниже, выводят данные в фиксированном формате: #include <time.h> /* ISO С */ char *asctime(const struct tm *tm); char *ctime(const time_t *timep); Как и в случае с gmtime () и local t ime (), asctime () и с time () возвращают указатели на статические буфера, которые могут быть перезаписаны после каждого вызова. Более того, эти две процедуры возвращают строки в одном и том же формате. Они отличаются лишь видом принимаемых аргументов, asctime() и ctime() должны использоваться тогда, когда все, что вам нужно, это простые сведения о дате и времени: #include <stdio.h> #include <time.h> int main(void) { time_t now; time(& now); printf("%s", ctime(& now)); } После запуска эта программа выводит результат в виде: "Thu May 22 15:44:21 2003'. Завершающий символ конца строки включен в результат. Точнее, возвращаемое значение указывает на массив из 26 символов, как показано на рис. 6.1.
172 Глава 6. Общие библиотечные интерфейсы - часть 1 О 1 2 3 4 5 б 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 I I *' . I ,11.11 I I II II III III I II III v T h u Ma у 2 2 1 5 : 4 4 : 2 1 2 0 0 3 An \0| Рис. 6.1. Возвращаемая функциями с time () и asctime () строка Значительная часть старого кода Unix полагается на тот факт, что значения в возвращенной строке имеют фиксированную позицию. При использовании этих функций помните, что они включают завершающий символ конца строки. Поэтому наш небольшой пример программы использует для print f () простую форматирующую строку "%s\ а не " % s \п", как можно было бы ожидать. ctimeO устраняет необходимость шага вызова localtimeO; в сущности, это эквивалентно ^ time_t now; char *curtime; time(& now); curtime = asctime(localtime(& now)); 6.1.3.2. Сложное форматирование времени: s trftimeO Хотя часто достаточно использования asctime() и ctimeO, у них есть также и ограничения: • Формат вывода фиксирован. Нет способа изменить порядок элементов. • В вывод не включаются сведения о часовом поясе. • 9 выводе используются сокращенные названия месяца и дня. • В выводе используются английские названия месяцев и дней. По этим причинам С89 ввело стандартную библиотечную процедуру strf time (): #include <time.h> /* ISO С */ size_t strftime(char *s, size__t max, const char *format, const struct tm *tm); strf time () сходна с sprintf (). Ее аргументы следующие: char *s Буфер для форматированной строки. size_t max Размер буфера, const char *format Форматирующая строка. const struct tm *tm Указатель на struct tm, представляющий разложенное время, которое надо отформатировать.
6.1. Времена и даты 173 Форматирующая строка содержит символы букв, смешанные в описателями преобразования, указывающими, что должно быть помещено в строку, такими, как полное имя дня недели, час в соответствии с 24-часовым или 12-часовым циклом, наличие указателей a.m. или р.т1. и т. д. (Вскоре будут приведены примеры.) Если всю строку можно отформатировать с использованием не более max символов, возвращаемое значение представляет собой число символов, помещенных в s, не включая завершающий нулевой байт. В противном случае, возвращаемое значение ноль. В последнем случае содержание s «неопределенно». Следующий простой пример дает представление об использовании strf time (): #include <stdio.h> #include <time.h> int main(void) { char buf[100]; tinie_t now; struct tm *curtime; time(& now) ; . curtime = localtime(& now) ; (void) strftime(buf, sizeof buf, "It is now %A, %B %d, %Y, %I:%M %p", curtime); printf("%s\n", buf); exit@); } После запуска эта программа выводит что-то типа: It is now Thursday, May 22, 2003, 04:15 PM В табл. 6.2 предоставлен полный список описателей преобразования, их возможные альтернативные представления и их значения. Вдобавок стандарт С99 добавил к списку дополнительные описатели; новые для С99 описатели помечены символом V. Таблица 6.2. Описатели преобразования формата strf time () Описатель С99 Значение %а Локальное сокращенное название дня недели. % А Локальное полное название дня недели. %Ь Локальное сокращенное название месяца. %В Локальное полное название месяца. A.m. - от ante meridiem (до полудня), p.m. - от post meridiem (пополудни), американская система обозначения 12-часового цикла времени суток. - Примеч. перев.
174 Глава 6. Общие библиотечные интерфейсы - часть 1 Таблица 6.2. Описатели преобразования формата strf time () (Продолжение) Описатель %с, %С, %d, %D %е, %F %g %G %h %H, %I, %j %m, %M, %n %P %r %R %S, %t %T %u, %U, %V, %w, %W, %x, %Ec %EC %Od %Oe %OH %OI %Om %OM %OS %Ou %OU %OV %Ow %OW %Ex C99 V V V V V V V V V V V V V V Значение Локальное «подходящее» представление даты и времени. Век @0-99). День месяца @1-31). То же, что %m/ %d/ %y. День месяца. Одна цифра дополняется пробелом A-31). То же, что и %Y-%m-%d (формат даты ISO 8601). Две последние цифры, основанной на неделе года @0-99). Основанный на неделе год ISO 8601. То же, что и %Ь. Час в 24-часовом цикле @0-23). Час в 12-часовом цикле @1-12). День года @01-366). Месяц в виде числа @1-12). Минута в виде числа @0-59). Символ конца строки (*\п'). Локальное обозначение a.m./p.m. Локальное время в 12-часовом цикле. То же, что и %н: %М. Секунда в виде числа @0-60). Символ табуляции (' \ t'). То же, что и %н: %М: %s (формат ISO 8601). Число дня недели ISO 8601, понедельник = 1 A-7). Номер недели, первое воскресенье является первым днем недели 1 @0-53). Номер недели ISO 8601 @1-53). День недели как число, воскресенье = 0 @-6). Номер недели, первый понедельник является первым днем недели I @0-53). Локальное «подходящее» представление даты.
6.1. Временам даты 175 Таблица 6.2. Описатели преобразования формата strf time () (Продолжение) Описатель С99 Значение %Х, %ЕХ Локальное «подходящее» представление времени. %у, %Еу,%Оу Две последние цифры года @0-99). %Y, %EY Год как число. %Z Локальный часовой пояс или отсутствие символов, если сведения о часовом поясе недоступны. • %% Простой %. Локаль (locale) является способом описания местной специфики, принимая во внимание такие вещи, как язык, кодировка символов и значения по умолчанию для форматирования дат, времени, денежных сумм и т. д. Мы будем иметь с ними дело в главе 13 «Интернационализация и локализация». Пока достаточно понять, что результаты strf time () для одной и той же форматирующей строки могут быть разными в зависимости от настроек текущей локали. Версии, начинающиеся с %Е и %0, предназначены для «альтернативного представления». В некоторых локалях есть несколько способов представления одних и тех же сведений; эти описатели предоставляют доступ к дополнительным представлениям. Если определенная локаль не поддерживает альтернативного представления, strftimeO использует обычную версию. Многие Unix-версии date дают возможность предоставить в командной строке форматирующую строку, начинающуюся с символа '+'. Затем date форматирует текущие дату и время и выводит в соответствии с форматирующей строкой: $ date + 'It is now %A, %B %d, %Y, %1:ОД %p* It is now Sunday, May 25, 2003, 06:44 PM Большинство новых описателей С99 происходит от таких существующих реализаций date Unix. Описатели %п и %t не являются в С абсолютно необходимыми, поскольку символы табуляции и конца строки могут быть помещены в строку непосредственно. Однако в контексте форматирующей строки date для командной строки они имеют больше смысла. Поэтому они также включены в спецификацию strf time (). Стандарт ISO 8601 определяет (среди других вещей), как нумеруются недели в пределах года. В соответствии с этим стандартом недели отсчитываются с понедельника по воскресенье, а понедельник является днем недели 1, а не 0. Если неделя, в которой оказывается 1 января, содержит по крайней мере четыре дня нового года, она считается неделей 1. В противном случае, это последняя неделя предыдущего года с номером 52 или 53. Эти правила используются для вычислений описателей форматов %g, %G и %v. (Хотя ограниченным американцам, таким, как автор, эти правила могут показаться странными, они обычно повсюду используются в Европе.)
176 Глава 6. Общие библиотечные интерфейсы - часть 1 Многие из описателей стандартов дают результаты, специфичные для текущей локали. Вдобавок некоторые указывают, что они выдают «подходящее» представление для локали (например, %х). Стандарт С99 определяет значения для локали «С». Эти значения перечислены в табл. 6.3. Таблица 6.3. Значения локали «С» для определенных форматов strf time () Описатель Значение %а Первые три символа %А. %А Один из дней Sunday, Monday,..., Saturday. %b Первые три символа %В. %В Один из месяцев January, February,..., December. %с Тоже, что и %а %b %e %T %Y. , %р AM или РМ. %г Тоже, что и %I:%M:%S %р. %х То же, что и %m/%d/%y. %Х То же, что и %т. %Z Определяется реализацией. Должно быть очевидно, что strf time () предоставляет значительную гибкость и контроль над связанным с датой и временем выводом, во многом таким же образом, как printf О и sprint f (). Более того, strf time () не может переполнить буфер, поскольку она проверяет входной параметр размера, что делает ее более безопасной процедурой, чем sprintf (). В качестве простого примера рассмотрим создание файлов журнала программы, когда каждый час создается новый файл. Имя файла должно включать дату и время создания: /* Проверка ошибок для краткости опущена */ char fname[PATH_MAX] ; /* РАТН_МАХ находится в <limits.h> */ time_t now; struct tm *tm; int fd; t ime(& now); tm = localtime(&: now) ; strftime(fname, sizeof fname, "/var/log/myapp.%Y-%m-%d-%H: %M", tm) ; fd = creat(name, 0600); Формат год-месяц-день-час-минута вызывает сортировку файлов в том порядке, в каком они были созданы.
6.1. Времена и даты 177 ЗАМЕЧАНИЕ. Некоторые форматы данных более полезны, чем другие. Например, 12- часовое время двусмысленно, также, как чисто числовые форматы дат. (Что означает '9/11'? Это зависит от того, где вы живете.) Сходным образом, годы из двух цифр также являются плохой мыслью. Используйте strftime () благоразумно. 6.1.4. Преобразование разложенного времени в time_t Получение от системы значений «секунд с начала Эпохи» просто; именно так даты и времена хранятся в индексах и возвращаются с помощью time () и stat (). Эти значения также легко оценивать на равенство или посредством < и > для простых тестов раньше/позже. Однако, с датами, введенными людьми, не так легко работать. Например, многие версии команды touch позволяют предусмотреть дату и время, в которое touch должна установить время модификации или доступа к файлу (с помощью utimeO, как было описано в разделе 5.5.3 «Изменение отметок времени: utime () »). Преобразование даты, введенной человеком, в значение time_t трудно: надо принять во внимание високосные годы, учесть часовые пояса и т. д. Поэтому стандарт С89 ввел функцию mktime (): #include <time.h> /* ISO С */ time_t mktime(struct tm *tm) ; Для использования mktime () укажите в struct tm соответствующие значения: год, месяц, день и т. д. Если вы знаете, действовало ли для данной даты летнее время, установите соответствующим образом поле tm__isdst: 0 для «нет» и положительное значение для «да». В противном случае, используйте отрицательное значение для «не знаю». Поля tm_wday и tm_yday игнорируются. mktime () предполагает, что stuct tm представляет локальное время, не UTC. Она возвращает значение time__t, представляющее переданные дату и время, или (time_t) (-1), если данные дата/время не могут быть правильно представлены. После успешного возвращения все значения struct tm выверены на попадание в правильные диапазоны, a tm_wday и tm_yday также корректно установлены. Вот простой пример: I /* ch06-echodate.c демонстрирует mktime(). */ 2 3 #include <stdio.h> 4 #include <time.h> 5 6 int main(void) 7 { 8 struct tm tm; 9 time_t then; 10 II printf("Enter a Date/time as YYYY/MM/DD HH:MM:SS : "); 12 scanf("%d/%d/%d %d:%d:%d", 13 & tm. tm_year, & tm. tm_mon, & tm. tm__mday,
178 Глава 6. Общие библиотечные интерфейсы - часть 1 14 & tm.tmjiour, & tm.tm_.min, & tm.tm_sec); 15 16 /* Проверка ошибок значений для краткости опущена. */ 17 tm.tm_year -= 1900; 18 tm.tm_mon--; 19 2 0 tm.tm_isdst = -1; /* Не знаю о летнем времени */ 21 22 then = mktime(& tm); 23 24 printf("Got: %s", ctime(& then)); 25 exit@); 26 } В строке 11 запрашиваются дата и время, а в строках 12-14 соответствующие значения считываются. (В коде изделия возвращаемые scanf () значения должны проверяться.) Строки 17 и 18 компенсируют различную базу для лет и месяцев соответственно. Строка 20 указывает, что мы не знаем, представляют ли данные дата и время летнее время. Строка 22 вызывает mktime (), а строка 24 выводит результат преобразования. После компилирования и запуска мы видим, что это работает: $ ch06-echodate Enter a Date/time as YYYY/MM/DD HH:MM:SS : 2003/5/25 19:07:23 Got: Sun May 25 19:07:23 2003 6.1.5. Получение сведений о часовом поясе На ранних системах Unix сведения о часовом поясе внедрялись в ядро при компиляции. Правила перехода на летнее время обычно были жестко вшиты в код, что создавало трудности для пользователей вне Соединенных Штатов или в местах внутри Соединенных Штатов, в которых не осуществлялся переход на летнее время. В современных системах эти сведения выделены в двоичные файлы, которые читаются библиотекой С при извлечении функций, связанных со временем. Эта методика позволяет избежать необходимости в перекомпилировании библиотек и системных файлов при изменении правил и гораздо упрощает обновление правил. Интерфейс языка С для сведений о часовых поясах развивался в разных версиях Unix, как на System V, так и Berkley, пока, наконец, не был стандартизован POSIX следующим образом: #include <time.h> /* POSIX */ extern char *tzname[2]; extern long timezone; extern int daylight; void tzset(void); Функция tzset () проверяет переменную окружения TZ для получения сведений о часовом поясе и переходе на летнее время2. Если эта переменная не установлена, tzset ()
6.1. бремена и даты 179 использует «определенный в реализации часовой пояс по умолчанию», который скорее всего является часовым поясом машины, на которой вы работаоте. После вызова tzset () сведения о локальном часовом поясе доступны в нескольких переменных: extern char *tzname[2] Стандартное имя и имя летнего времени для часового пояса. Например, для областей США в восточном часовом поясе именами часового пояса являются 'EST' (Eastern Standard Time) и 'EDT (Eastern Daylight Time). extern long timezone , Разница в секундах между текущим часовым поясом и UTC. Стандарт не определяет, как эта разница работает. На практике отрицательные значения представляют часовые пояса восточнее (перед, или те, которые позже) UTC; положительные значения представляют часовые пояса западнее (за, или те, которые раньше) UTC. Если вы посмотрите на это значение как «насколько изменить местное время, чтобы оно стало равно UTC», тогда знак этого значения имеет смысл. extern int daylight Эта переменная равна нулю, если правила перехода на летнее время никогда не должны использоваться для данного часового пояса, и не равны нулю в противном случае. IЗАМЕЧАНИЕ. Переменная daylight не означает, действует ли в настоящий момент летнее время! Вместо этого она просто констатирует, может ли текущий часовой пояс вообще иметь летнее время. Стандарт POSIX указывает, что ctime (), localtime (), mktime () и strf time () действуют, «как если бы» они вызывали tzset (). Это означает, что им в действительности не нужно вызывать tzset (), но они должны вести себя, как если бы эта функция была вызвана. (Формулировка призвана дать определенную гибкость при реализации, в то же время гарантируя правильное поведение кода уровня пользователя.) На практике это означает, что вы сами почти никогда не будете вызывать tzset С). Но если понадобится, эта функция есть. 6.1.5.1. Системы BSD: timezone О, не timezone Некоторые производные от BSD 4.4 системы вместо переменной POSIX timezone предоставляют функцию timezone (): #include <time.h> /* BSD */ char *timezone(int zone, int dst); Аргумент zone является числом минут западнее GMT, a dst истинно, если действует летнее время. Возвращаемое значение является строкой, дающей имя указанного Хотя POSIX стандартизует формат TZ, он не представляет интереса, поэтому мы не стали возиться здесь сего документированием. В конце концов, именно tzset () должна понимать формат, а не код пользователя. Реализации могут использовать и используют форматы, которые расширяют POSIX. - Примеч. автора
180 Глава 6. Общие библиотечные интерфейсы - часть 1 часового пояса, или значение, выраженное относительно GMT. Эта функция обеспечивает совместимость с функцией V7 с тем же именем и поведением. Локальное время: откуда оно известно? Системы GNU/Linux хранят информацию о часовых поясах в файлах и каталогах в/usr/share/zoneinfo: $ cd /usr/share/zoneinfo $ Is -FC Africa/ Canada/ Factory Iceland MST7MDT Portugal W-SU America/ Chile/ GB Indian/ Mexico/ ROC WET Antarctica/ Cuba GB-Eire Iran Mideast/ ROK Zulu Arctic/ EET GMT Israel NZ Singapore iso3166.tab Asia/ EST GMT+0 Jamaica NZ-CHAT SystemV/ posix/ Atlantic/ EST5EDT GMT-0 Japan Navajo Turkey posixrules Australia/ Egypt GMTO Kwajalein PRC UCT right/ Brazil/ Eire Greenwich Libya PST8PDT US/ zone.tab CET Etc/ HST MET Pacific/ UTC CST6CDT Europe/ Hongkong MST Poland Universal Когда возможно, этот каталог использует прямые ссылки для предоставления одних и тех же данных с разными именами. Например, файлы EST5EDT и US /Eastern на самом деле одни и те же: $ Is -il EST5EDT US/Eastern 724350 -rw-r--r-- 5 root root 1267 Sep 6 2002 EST5EDT 724350 -rw-r—r-- 5 root root 1267 Sep 6 2002 US/Eastern Частью установки системы является выбор часового пояса. Надлежащий файл данных часового пояса помещается затем в /etc/localtime: $ file /etc/localtime /etc/localtime: timezone data v На нашей системе это автономная копия файла для нашего часового пояса. На других системах это может быть символическая ссылка на файл в /usr/share/zoneinfo. Преимуществом использования отдельной копии является то, что все по-прежнему работает, если /usr не смонтирован. Переменная окружения TZ, если она установлена, перекрывает значение по умолчанию для часового пояса: $ date /* Дата и время в часовом поясе по умолчанию */ ' Wed Nov 19 06:44:50 EST 2003 $ export TZ=PST8PDT /* Смена часового пояса на Западное побережье США */ $ date /* Вывести дату и время */ Wed Nov 19 03:45:09 PST .2003
6.2. Функции сортировки и поиска 181 Широкое распространение этой функции делает переносимое использование переменной POSIX time zone трудной. К счастью, мы не видим большой потребности в ней: strf time () должно быть достаточно едва ли не для большинства необычных потребностей. 6.2. Функции сортировки и поиска Сортировка и поиск являются двумя фундаментальными операциями, потребность в которых постоянно возникает во многих приложениях. Библиотека С предоставляет ряд стандартных интерфейсов для осуществления этих задач. Все процедуры разделяют общий лейтмотив; данные управляются через указатели void*, а сортировку осуществляют предоставленные пользователем функции. Обратите также внимание, что эти API применяются к данным в. памяти. Структуры сортировки и поиска в файлах значительно более сложны и выходят за рамки вводного руководства, подобного данному. (Однако, команда sort хорошо работает для текстовых файлов; см. справочную страницу для sort{\). Сортировка двоичных файлов требует написания специальной программы.) Поскольку ни один алгоритм, не работает одинаково хорошо для всех приложений, имеются несколько различных наборов библиотечных процедур для сопровождения искомых коллекций данных. Данная глава рассматривает лишь один простой интерфейс для поиска. Другой, более развитый интерфейс описан в разделе 14.4 «Расширенный поиск с использованием двоичных деревьев». Более того, мы намеренно не объясняем лежащие в основе алгоритмы, поскольку данная книга об API, а не об алгоритмах и структурах данных. Важно понять, что API можно рассматривать как «черные ящики», выполняющие определенную работу без необходимости понимания подробностей их работы. 6.2.1. Сортировка: qsort () Сортировка выполняется с помощью qsort (): #include <stdlib.h> /* ISO С */ void qsort(void *base, size_t nmemb, size_t size, int (*compare)(const void *, const void *)); Название qsort () происходит от алгоритма машинного поиска Хоара Quicksort (C.A.R. Hoare's Quicksort algorithm), который использовался в первоначальной реализации Unix. (Ничто в стандарте POSIX не требует использования этого алгоритма для qsort (). Реализация GLEBC использует высоко оптимизированную комбинацию Quicksort и Insertion Sort.) qsort () сортирует массивы произвольных объектов. Она работает, перетасовывая непрозрачные участки памяти из одного места массива в другой и полагаясь на то, что вы, программист, предоставите функцию сравнения, которая позволяет определить относительное расположение одного элемента массива относительно другого. Аргументы следующие:
182 Глава 6. Общие библиотечные интерфейсы - часть 1 void *baee Адрес начала массива. size_t nmemb Общее число элементов в массиве. size_t size Размер каждого элемента массива. Лучший способ получения этого значения - оператор С sizeof. int (^compare)(const void*, const void*) Возможно устрашающее объявление указателя функции. Оно говорит, что «с ompar e указывает на функцию, которая принимает два параметра 'const void *' и возвращает int». Большая часть работы заключается в написании соответствующей функции сравнения. Возвращаемое значение должно имитировать соответствующее значение strcmp (): меньше нуля, если первое значение «меньше» второго, ноль, если они равны, и больше нуля, если первое значение «больше» второго. Именно функция сравнения определяет значения «больше» и «меньше» для всего, что вы сравниваете. Например, для сравнения двух значений double мы могли бы использовать эту функцию: int dcomp(const void *dlp/ const void *d2p) { const double *dl, *d2; dl = (const double *) dip; /* Привести указатели к нужному типу */ d2 = (const double *) d2p; if (.*dl < *d2) /* Сравнить и вернуть нужное значение */ return -l; else if (*dl > *d2) return 1; else if (*dl == *d2) return 0 else return -1; /* NaN сортируется до„действительных чисел */ } Это показывает общий стереотип для функций сравнения: привести тип аргументов от void* к указателям на сравниваемый тип, а затем вернуть результат сравнения. Для чисел с плавающей точкой простое вычитание, подобное 'return *dl - *d2\ не работает, особенно если одно значение очень маленькое или одно или оба значения являются специальными значениями «не число» или «бесконечность». Поэтому нам приходится осуществлять сравнение вручную, принимая во внимание нечисловое значение (которое даже не равно самому себе!)
6.2. Функции сортировки и поиска 183 6.2.1.1. Пример: сортировка сотрудников Для более сложных структур требуются более сложные функции. Например, рассмотрите следующую (довольно тривиальную) struct employee: struct employee { char lastname[30]; char firstname[30]; long emp_id; time_t start_date; }; Мы могли бы написать функцию для сортировки сотрудников по фамилии, имени и идентификационному номеру: int emp_name_id__compare (const void *elp, const void *e2p) { const struct employee *el, *e2; int last, first; el = (const struct employee *) elp; /* Преобразовать указатели */ e2 = (const struct employee *) e2p; if ((last = strcmp(el->lastname, e2->lastname)) != 0) /* Сравнить фамилии */ return last; /* Фамилии различаются */ /* фамилии совпадают/ сравнить имена */ if ((first = strcmp(el->firstname, e2->firstname)) != 0) /* Сравнить имена */ return first; /* Имена различаются */ /* имена совпадают, сравнить номера ID */ if (el->emp_id < e2->emp_id) /* Сравнить ID сотрудника */ return -1; else if (el->emp_id == e2->emp_id) return 0; else return 1; } Логика здесь проста: сначала сравниваются фамилии, затем имена, а затем номера ID, если два имени совпадают. Используя для строк strcmp (), мы автоматически получаем правильное отрицательное/нулевое/положительное значение для возвращения. При сравнении ID сотрудников нельзя просто использовать вычитание: представьте, что long 64-разрядный, a int 32-разрядный, а два значения отличаются лишь в старших 32 битах (скажем, младшие 32 бита равны нулю). В таком случае вычитание автоматически привело бы к приведению типа к int с отбрасыванием старших 32 битов и возвращением неверного результата.
184 Глава 6. Общие библиотечные интерфейсы - часть 1 13АМЕЧАНИЕ. Возможно, мы остановились при сравнении имен, в этом случае все I сотрудники с совпадающими фамилиями и именами оказались бы сгруппированы, но I никак не отсортированы. \Это важный момент: qsort () не гарантирует стабильной сортировки. Стабиль- Хна сортировка, в которой, если два элемента равны на основе значения какого-либо \ключа(-ей); они сохраняют свой первоначальный порядок друг относительно друга I в конечном отсортированном массиве. Например, рассмотрите трех сотрудников ' I с одинаковыми фамилиями и именами и с номерами 17, 42 и 81. Их порядок в первоначальном массиве, возможно, был 42, 81 и 17. (Что означает, что сотрудник 42-т*^ Хходится по индексу с меньшим значением, чем сотрудник 81, который, в свою I очередь, находится по индексу с меньшим значением, чем сотрудник 17.) После I сортировки порядок может оказаться 81, 42 и 17. Если это представляет пробле- \му, процедура сравнения должна рассматривать все важные ключевые значения. I (Наша так и делает.) Просто используя другую функцию, мы можем отсортировать сотрудников по старшинству: int emp_seniority_compare (const void *elp, const void *e2p) { const struct employee *el,*e2; double diff; /* Привести указатели к нужному типу */ el = (const struct employee *) elp; e2 = (const struct employee *) e2p; /* Сравнить времена */ diff = difftime(el->start_date, e2->start_date); if (diff < 0) return -1; else if (diff > 0) return 1; else return 0; } Для максимальной переносимости мы использовали difftimeO, которая возвращает разницу в секундах между двумя значениями time_t. Для данного конкретного случая приведение, такое, как return (int) difftime(el->start_date, e2->start_date); должно сработать, поскольку значения time_t находятся в приемлемом диапазоне. Тем не менее, мы вместо этого использовали полный трехсторонний оператор if, просто из предосторожности. Вот пример файла данных со списком пяти президентов США:
6.2. Функции сортировки и поиска 185 $ cat presdata.txt /* Фамилия, имя, номер президента, инаугурация */ Bush George 43 980013600 Clinton William 42 727552800 Bush George 41 601322400 Reagan Ronald 40 348861600 Carter James 39 222631200 В ch06-sortemp.c приведена простая программа, которая считывает этот файл в массив struct employee, а затем сортирует его, используя две только что представленные функции сравнения. I /* ch06-sortemp.c Демонстрирует qsort() с двумя функциями сравнения. */ 2 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <time.h> 6 7 struct employee { 8 char lastname[30]; 9 char firstname[30] ; 10 long emp_id; II time_t start__date; 12 }; ' 13 14 /* emp__name_id_compare сравнение по имени, затем по ID */ 15 16 int emp_name_id_compare (const void *elp, const void *e2p) 17 { /* ...как показано ранее, опущено для экономии места... */ 39 } 40 41 /* emp_seniority_compare сравнение по старшинству */ 42 43 int emp_seniority__compare (const void *elp, const void *e2p) 44 { /* ...как показано ранее, опущено для экономии места... */ 58 } 59 60 /* main демонстрация сортировки */ 61 62 int main(void) 63 { 64 #define NPRES 10 65 struct employee presidents[NPRES] ; 66 int i, npres;
186 Глава 6. Общие библиотечные интерфейсы - часть 1 67 char buf[BUFSIZ]; 68 69 /* Очень простой код для чтения данных: */ 70 for (npres = 0;npres < NPRES && fgets(buf,BUFSIZ,stdin) != NULL; 71 npres++) { 72 sscanf(buf, "%s %s %ld %ld\n", 73 presidents[npres].lastname, 74 presidents[npres].firstname, 75 & presidents[npres].emp_id/ 76 & presidents[npres].start_date); 77 } 78 7 9 /* npres теперь содержит число прочитанных строк. */ 80 81 /* Сначала сортировка по имени */ 82 qsort(presidents, npres, sizeof(struct employee), emp_name_id_compare); 83 84 /* Вывести результат */ 85 printf ("Sorted by name:\n"); 86 for (i = 0; i < npres; i++) 87 printf("\t%s %s\t%d.\t%s" , 88 presidents[i].lastname, 89 presidents[i].firstname, 90 presidents[i].emp_id, 91 ctime(& presidents[i].start_date)); 92 93 /* Теперь сортировка по старшинству */ 94 qsort(presidents, npres, sizeof(struct employee), emp_seniority_compare); 95 96 /* И снова вывести */ 97 printf("Sorted by seniority:\n"); 98 for (i = 0; i < npres; i++) 99 printf("\t%s %s\t%d\t%s", 100 presidents[i].lastname, 101 presidents[i].firstname, 102 presidents[i].emp_id, 103 ctime(& presidents[i].start_date)); 104 } Строки 70-77 считывают данные. Обратите внимание, что любое использование scanf () требует от входных данных «хорошего поведения». Если, например, какое- нибудь имя содержит более 29 символов, возникает проблема. В данном случае, мы вне опасности, но в коде изделия нужно быть гораздо более осмотрительным.
6.2. Функции сортировки и поиска 187 Строка 82 сортирует данные по имени и по ID сотрудника, а затем строки 84-91 выводят отсортированные данные. Сходным образом строка 94 пересортировывает данные, на этот раз по старшинству, а строки 97-103 выводят результаты. После компилирования и запуска программа выдает следующие результаты: $ ch06-sortemp < presdata.txt Sorted by name: Bush George 41 Fri Jan 20 13:00:00 1989 , Bush George 43 Sat Jan 20 13:00:00 2001 Carter James 39 Thu Jan 20 13:00:00 1977 Clinton William 42 Wed Jan 20 13:00:00 1993 Reagan Ronald 40 Tue Jan 20 13:00:00 1981 Sorted by seniority: Carter James 39 Thu Jan 20 13:00:00 1977 . Reagan Ronald 40 Tue Jan 20 13:00:00 1981 Bush George 41 Fri Jan 20 13:00:00 1989 Clinton William 42 Wed Jan 20 13:00:00 1993 Bush George 43 Sat Jan 20 13:00:00 2001 (Мы использовали 1 час пополудни как приблизительное время, когда все президенты начали работать3.) Стоит заметить одну вещь: qsort () переставляет данные в массиве. Если каждый элемент массива представляет собой большую структуру, при сортировке массива большое количество данных будут копироваться туда-сюда. Вместо этого может оказаться выгодным создать отдельный массив указателей, каждый из которых указывает на один элемент массива. Затем использовать qsort () для сортировки массива указателей, получая доступ к несортированным данным через сортированные указатели. Платой за это является дополнительная память для размещения указателей и модификация функций сравнения для дополнительного перенаправления указателей при сравнении структур. Полученной выгодой может стать значительное ускорение работы, поскольку на каждом шаге перемещается лишь четырех- или восьмибайтный указатель вместо большой структуры. (Наша struct employee имеет размер по крайней мере 68 байтов. При обмене четырехбайтных указателей перемещается в 17 раз меньше данных, чем при обмене структур.) Для тысяч размещенных в памяти структур разница может быть существенной. ^ЗАМЕЧАНИЕ. Если вы являетесь программистом C++, знайте! qsort () может \быть опасной для использования с массивами объектов! qsort () осуществляет продетые перемещения памяти, копируя байты. Она совершенно ничего не знает I о конструкциях C++, таких, как конструкторы копирования или функции opera tor= (). I Вместо этого используйте одну из функций сортировки STU1 или используйте методику I отдельного массива указателей. a. STL (Standard Template Library, стандартная библиотека шаблонов). - Примеч. науч. ред. Вывод, показанный здесь, относится к U.S. Eastern Standard Time. Вы получите различные результаты для одних и тех же программ и данных, если используете другой часовой пояс. - Примеч. автора.
188 Глава 6. Общие библиотечные интерфейсы - часть 1 6.2.1.2. Пример: сортировка содержимого каталога В разделе 5.3 «Чтение каталогов» мы продемонстрировали, как элементы каталогов возвращаются в физическом порядке каталога. В большинстве случаев гораздо полезнее иметь содержимое каталога отсортированным каким-нибудь образом, например, по имени или по времени изменения. Хотя и не стандартизованные POSIX, несколько процедур упрощают это, используя qsort () в качестве лежащего в основе сортирующего агента: #include <dirent.h> /* Обычный */ int scandir (const char Mir, struct dirent ***namelist, int (*select)(const struct dirent *), int (*corapare)(const struct dirent **, const struct dirent **)); int alphasort(const void *a, const void *b); int versionsort(const void *a, const void *b); /* GLIBC */ Функции scandir() и alphasort () были сделаны доступными в 4.2 BSD и широко поддерживаются4, versionsort () является расширением GNU. scandir () читает каталог, имя которого дано в dir, создает с использованием malloc () массив указателей struct dirent и устанавливает *namelist, чтобы он указывал на начало этого массива. Как массив указателей, так и указываемые структуры struct dirent выделяются с помощью malloc (); вызывающий код должен использовать free (), чтобы избежать утечек памяти. Для выбора нужных элементов используйте указатель функции select. Когда это значение равно NULL, все действительные элементы каталога включаются в конечный массив. В противном случае (* select) () вызывается для каждого элемента, и те элементы, для которых она возвращает ненулевое (истинное) значение, включаются в массив. Указатель функции compare сравнивает два элемента каталога. Он передается функции qsort () для использования при сортировке. alphasort () лексикографически сравнивает имена файлов. Она использует для сравнения функцию strcoll (). strcoll () похожа на strcmp (), но учитывает связанные с местной спецификой правила сортировки (см. раздел 13.4 «Не могли бы вы написать это для меня по буквам?»). versionsort () является расширением GNU, которое использует для сравнения имен файлов функцию GNU strverscmpO (см. strverscmpC).) Короче говоря, эта функция понимает обычные соглашения по версиям имен файлов и сравнивает их соответствующим образом. В ch06-sortdir. с приведена программа, похожая на ch04-catdir. с. Однако, она использует для работы scandir () и alphasort (). 1 /* ch06-sortdir .с Демонстрирует scandir(), alphasort () . */ 2 3 #include' <stdio.h> /* для printf() etc. */ 4 Заметным исключением является лишь Sun Solaris, где эти две функции существуют лишь в трудной для использования библиотеке совместимости с BSD. - Примеч. автора.
6.2. Функции сортировки и поиска 4 #include <errno.h> /* для errno */ 5 #include <sys/types.h> /* для системных типов */ 6 #include <dirent.h> /* для функций каталогов */ 7 8 char *myname; 9 int process(const char *dir); 10 11 /* main перечислить аргументы каталога */ 12 13 int main(int argc, char **argv) 14 { 15 int i; 16 int errs = 0; 17 18 myname = argv[0]; 19 20 if (argc == 1) 21 errs = process("."); /* по умолчанию текущий каталог * 22 else 23 for (i = 1; i < argc; i++) 24 errs += process(argv[i]); 25 26 return (errs != 0); 27 } 28 29 /* nodots игнорирует файлы с точкой, для scandirO */ 30 31 int 32 nodots(const struct dirent *dp) 33 { 34 return (dp->d__name [0] !='.*); 35 } 36 37^ / * 38 * process сделать что-то с каталогом, в данном случае, 39 * вывести в стандартный вывод пары индекс/имя.. 40 * Вернуть 0, если все нормально, в противном случае 1. 41 */ . 42 43 int 44 process(const char *dir) 45 { 46 DIR *dp;
190 Глава 6. Общие библиотечные интерфейсы - часть 1 47 struct dirent **entries; 48 int nents, i; 49 50 nents = scandir(dir, & entries, nodots, alphasort); 51 if (nents < 0) { 52 fprintf(stderr, "%s: scandir failed: %s\n", myname, 53 strerror(errno)); 54 return 1; 55 } 56 57 for (i = 0; i < nents; i++) { 58 printf("%81d %s\n", entries[i]->d_ino, entries[i]->d_name); 59 free(entries[i]); 60 } ' 61 62 free(entries); 63 64 return 0; 65 } Функция main () программы (строки 1-27) следует стандартному шаблону, который мы использовали до этого. Функция nodots () (строки 31-35) действует как параметр select, выбирая лишь имена файлов, которые не начинаются с точки. Функция process () (строки 43-65) довольно проста, причем scandir () делает большую часть работы. Обратите внимание, как каждый элемент отдельно освобождается с помощью f гее () (строка 59) и как освобождается также весь массив (строка 62). При запуске содержимое каталога в самом деле выводится в отсортированном порядке, без 4.' и '. .': $ ch06-sortdir /* Действия по умолчанию отображают текущий каталог */ 2 09717 6 00-preface.texi 2097187 01-intro.texi 2097330 02-cmdline.texi 2097339 03-memory.texi 2097183 03-memory.texi.save 2097335 04-fileio.texi 2097334 05-fileinfo.texi 2097332 06-generall.texi 6.2.2. Бинарный поиск: bsearch() Линейный поиск в значительной степени похож на свое название: вы начинаете в начале и проходите искомый массив, пока не встретите то, что нужно. Для чего-нибудь простого, типа поиска целых, это обычно принимает форму цикла for. Рассмотрите эту функцию:
6.2. Функции сортировки и поиска 191 /* ifind линейный поиск, возвращает найденный индекс или -1 */ int ifind(int х, const int array[], size_t nelems) { size_t i; for (i = 0; i < nelems; i++) if (arrayfi] == x) /* найдено */ return i; return -1; } Преимуществом линейного поиска является его простота; легко с самого начала написать правильный код. Более того, он работает всегда. Даже если в конец массива добавляются элементы или они удаляются из него, нет необходимости сортировать массив. Недостатком линейного поиска является то, что он медленный. В среднем для массива, содержащего nelems элементов, при линейном поиске случайного элемента требуется 'nelems/2' сравнений, прежде чем найдется нужный элемент. Это становится чрезмерно дорогим даже на современных высокопроизводительных системах, когда nelems принимает большие значения. Поэтому линейный поиск следует использовать лишь с небольшими массивами. В отдичие от линейного, бинарный поиск требует, чтобы входной массив был уже отсортирован. Недостатком здесь является то, что если добавляются элементы, массив перед новым поиском нужно повторно отсортировать. (Когда элементы удаляются, остальное содержимое массива все равно должно быть перетасовано. Это не так дорого, как повторная сортировка, но все равно может потребовать большого перемещения данных.) Преимуществом бинарного поиска, и значительным, является то, что бинарный поиск умопомрачительно быстр, требуя самое большее logiiN) сравнений, где N является числом элементов в массиве. Функция bsearch () объявлена следующим образом: #include <stdlib.h> /* ISO С */ void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compare)(const void *, const void *)); Параметры и их назначение сходны с таковыми для qsort (): const void *key Объект, который ищется в массиве. const void *base Начало массива. size_t nmemb Число элементов в массиве. size_t size Размер каждого элемента, полученный с помощью sizeof.
192 Глава 6. Общие библиотечные интерфейсы - часть 1 int (*compare)(const void*, const void*) Функция сравнения. Она должна работать таким же образом, как функция сравнения для qsort (), возвращая отрицательные/нулевые/положительные значения в соответствии с тем, меньше/равен/больше первый параметр по сравнению со вторым. Если объект не найден, bsearch () возвращает NULL. В противном случае она возвращает указатель на найденный объект. Если key соответствует более одного объекта, какой из них будет возвращен, не определено. Поэтому, как и в случае с qsort (), убедитесь, что функция сравнения принимает во внимание все существенные части искомой структуры данных. спОб-searchemp. с показывает bsearch () на практике, расширяя использованный ранее пример struct employee. 1. /* ch06-searchemp.c Демонстрация bsearch(). */ 2 3 #include <stdio.h> 4 #include.<errno.h> 5 #include <stdlib.h> 6 7 struct employee { 8 char lastname[30] ; 9 char firstname[30]; 10 long emp_id; 11 time_t start_date; 12 }; 13 14'/* emp_id_compare сравнение по ID */ 15 16 int emp_id_compare(const void *elp, const void *e2p) 17 { 18 const struct employee *el, *e2; 19 2 0 el = (const struct employee *) elp; 21 e2 = (const struct employee *') e2p; 22 2 3 if (el->emp_id < e2->emp_id) 24 return -1; 25 else if (el->emp_id == e2->emp_id) 2 6 return 0; 27 else 2 8 return 1; 29 } 30 31 /* print_employee напечатать структуру сотрудника */ 32 33 void print_employee(const struct employee *emp)
6.2. Функции сортировки и поиска 193 34 { 35 printf("%s %s\t%d\t%s", emp->lastname, emp->firstname, 36 emp->emp_id, ctime(& emp->start_date)); 37 } Строки 7-12 определяют struct employee; она та же, что и раньше. Строки 16-29 служат в качестве функции сравнения как для qsort (), так и для bsearch (). Они сравнивают лишь ID сотрудников. Строки 33-37 определяют print_employee (), которая является удобной функцией для печати структуры, поскольку это делается из разных мест. 39 /* main --- демонстрация сортировки'*/ 40 41 in.t main(int argc, char **argv) 42 { 43 #define NPRES 10 44 struct employee presidents[NPRES] ; 4 5 int i, npres; 46 char buf[BUFSIZ]; 47 struct employee *the_pres; 48 struct employee key; 49 int id; 50 FILE *fp; 51 52 if (argc != 2) { 53 fprintf(stderr, "usage: %s datafile\n", argv[0]); 54 exit(l); 55 } 56 57 if ((fp = fopen(argv[l], "r")) == NULL) { 58 fprintf(stderr, "%s: %s: could not open: %s\n", argv[0], 59 argv[l]/ strerror(errno)); 60 exit(l); 61 } ' . 62 63 /* Очень простой код для чтения данных: */ 64 for (npres = 0; npres < NPRES && fgets(buf, BUFSIZ, fp) != NULL; 65 npres++) { 66 sscanf(buf, "%s %s %ld %ld", 67 presidents[npres].lastname, 68 presidents[npres].firstname, 69 & presidents[npres].emp_id, 70 & presidents[npres].start_date); 71 } 72 fclose(fp); 73 7-159
194 Глава 6. Общие библиотечные интерфейсы - часть 1 7 4 /* В npres теперь число действительно прочитанных строк. */ 75 76 /* Сначала отсортировать по id */ 77 qsort (presidents, npres, sizeof (struct employee), emp_.id_compa.re); 78 79 /* Напечатать результат */ 80 printf("Sorted by ID:\n"); 81 for (i = 0; i < npres; i++) { 82 putchar(*\t'); 83 print_employee(& presidents[i]); 84 } 85 86 for (;;) { 87 printf("Enter ID number: "); 88 ' if (fgets(buf, BUFSIZ, stdin) ==mJLL) 89 break; 90 91 sscanf(buf, "%d\n", & id); 92 key.emp_id = id; 93 the_pres = (struct employee *) bsearch(& key, presidents, 94 npres, sizeof(struct employee), emp_id_compare); 95 96 if (the_pres != NULL) { 97 printf ( "Found: " ) ;. 98 print_employee(the_pres); 99 } else 100 printf("Employee with ID %d not found!\n", id); 101 } 102 103 putchar('\n'); /* Напечатать в конце символ новой строки. */ 104 105 exit@); 106 } Функция main () начинается с проверки аргументов (строки 52-55). Затем она читает данные из указанного файла (строки 57-72). Стандартный ввод для данных сотрудников использоваться не может, поскольку он зарезервирован для запроса у пользователя ID искомого сотрудника. Строки 77-84 сортируют, а затем печатают данные. Затем программа входит в цикл, начинающийся со строки 86. Она запрашивает идентификационный номер сотрудника, выходя из цикла по достижению конца файла. Для поиска в массиве мы используем struct employee с именем key. Достаточно лишь установить в его поле eirip_id введенный номер ID; другие поля при сравнении не используются (строка 92).
6.3. Имена пользователей и групп 195 Если найден элемент с подходящим ключом, bsearch() возвращает указатель на него. В противном случае она возвращает NULL. Возвращенное значение проверяется в строке 96, и осуществляется нужное действие. Наконец, строка 102 выводит символ конца строки, чтобы системное приглашение появилось с новой строки. Вот что появляется после компилирования и запуска программы: $ ch06-searchexnp presdata.txt /* Запуск программы */ Sorted by ID: Carter James 39 Thu Jan. 20 13:00:00 1977 Reagan Ronald 40 Tue Jan 20 13:00:00 1981 > Bush George 41 Fri Jan 20 13:00:00 1989 . Clinton William 42 Wed Jan 20 13:00:00 1993 Bush George 43 Sat Jan 20 13:00:00 2001 Enter ID number: 42 /* Ввод действительного номера */ Found: Clinton William 42 Wed Jan 20 13:00:00 1993 /* Найдено */ Enter ID number: 29 /* Ввод неверного номера */ Employee with ID 29 not found! /* He найдено */ Enter ID number: 40 /* Попытка другого верного номера */ Found: Reagan Ronald 40 Tue Jan 20 13:00:00 1981 /* Этот тоже найден */ Enter ID number: AD /* CTRL-D в качестве конца файла */ $ /* Готов к приему следующей команды */ Дополнительный, более продвинутый API для поиска коллекций данных описан в разделе 14.4 «Расширенный поиск с использованием двоичных деревьев». 6.3. Имена пользователей и групп Хотя операционная система для сохранения владельцев файлов и проверки прав доступа работает с идентификационными номерами пользователей и групп, люди предпочитают работать с именами пользователей и групп. Ранние системы Unix хранили информацию, сопоставляющую имена с номерами ID, в простых текстовых файлах /etc/passwd и /etc/group. На современных системах эти файлы до сих пор существуют, а их формат не изменился после V7 Unix. Однако, они больше не определяют данные полностью. Большие установленные системы с множеством сетевых хостов хранят сведения в сетевых базах данных, которые представляют собой способ хранения информации на небольшом числе серверов, доступ к которым осуществляется через сеть5. Однако, такое использование прозрачно для большинства приложений, поскольку доступ к информации осуществляется через тот самый API, который использовался для получения сведений из текстовых файлов. Именно по этой причине Типичные сетевые базы данных включают Network Information Service (NIS) и NIS+ от Sun Microsystems, Kerberos (Hesiod), MacOS X Netlnfo (версии вплоть до и включая 10.2) и LDAP, Lightweight Directory Access Protocol. Системы BSD хранят сведения в базах данных на диске и автоматически создают файлы /etc/passwd и /etc/group.- Примеч. автора
196 Глава 6. Общие библиотечные интерфейсы - часть 1 POSIX стандартизует лишь API; в совместимой с POSIX системе файлы /etc/passwd и /etc/group не обязательно должны существовать. API для этих двух баз данных похожи; большая часть обсуждения фокусируется на базе данных пользователей. 6.3.1. База данных пользователей Традиционный формат /etc/passwd поддерживает по одной строке на пользователя. В каждой строке есть несколько полей, каждое из которых отделено от следующего символом двоеточия: $ grep arnold /etc/passwd arnold:x:2076:10:Arnold D. Robbins:/home/arnold:/bin/bash По порядку эти поля следующие: Имя пользователя Это то, что пользователь набирает при регистрации, что отображается с помощью'Is . -1', а также используется в любом другом контексте при отображении пользователей. Поле пароля На старых системах здесь хранился зашифрованный пароль пользователя. На новых системах в этом поле скорее всего стоит х (как в данном случае), это означает, что сведения о пароле находятся в другом файле. Это разделение является средством обеспечения безопасности; если непривилегированному пользователю недоступен зашифрованный пароль, его значительно сложнее «взломать». ID пользователя Должен быть уникальным; один номер на пользователя. ID группы Это номер ID начальной группы пользователя. Как обсуждается далее, на современных системах с процессами связаны несколько групп. Настоящее имя пользователя Это по крайней мере имя и фамилия пользователя. Некоторые системы допускают разделяемые запятыми поля для местоположения офиса, номера телефона и т. д., но это не стандартизовано. Входной каталог Этот каталог становится домашним каталогом для пользователей, когда они зарегистрируются в системе ($НОМЕ - по умолчанию для команды cd). Входная программа Программа, которая запускается при регистрации пользователя. Обычно это оболочка, но не обязательно. Если это поле оставлено пустым, по умолчанию используется /bin/sh.
6.3. Имена пользователей и групп 197 Доступ к базе данных пользователей осуществляется через процедуры, объявленные в <pwd. h>: #include <sys/types.h> /* XSI */ #include <pwd.h> struct passwd *getpwent(void); void setpwent(void); void endpwent(void); struct passwd *getpwnam(const char *name); struct passwd *getpwuid(uid__t uid) ; Поля в struct passwd, использующиеся различными процедурами API, напрямую соответствуют полям файла паролей: struct passwd { char *pw__name; /* имя пользователя */ char *pw_passwd; /* пароль пользователя */ uid_t pw_uid; /* id пользователя */ gid_t pw_gid; /* id группы */ char *pw_gecos; /* настоящее имя */ char *pw__dir; /* домашний каталог */ char *pw_shell; /* программа оболочки */ }; (Имя pw_gecos историческое; когда разрабатывались ранние системы Unix, это поле содержало соответствующие сведения для учетной записи пользователя на системах Bell Labs Honeywell с операционной системой GECOS.) Назначение каждой процедуры описано в следующем списке. struct passwd *getpwent(void) Возвращает указатель на внутреннюю структуру static struct passwd, содержащую сведения о «текущем» пользователе. Эта процедура читает всю базу данных паролей, по одной записи за раз, возвращая указатель на структуру для каждого пользователя. Каждый раз возвращается тот же самый указатель; т. е. для каждой записи пользователя внутренняя struct passwd переписывается заново. Когда getpwent () достигает конца базы данных паролей, она врзвращает NULL. Таким образом, она позволяет пройти через всю базу данных по одному пользователю за раз. Порядок, в котором возвращаются записи, не определен. void setpwent(Void) Сбрасывает внутреннее состояние, так что следующий вызов getpwent () возвращает первую запись в базе данных паролей. void endpwent(void) «Закрывает базу данных», так сказать, будь то простой файл, сетевое соединение или что-нибудь еще. struct passwd *getpwnam(const char *name) Ищет пользователя с членом pw_name, соответствующим name, возвращая указатель на static struct passwd, описывающий пользователя, или NULL, если пользователь не найден.
198 Глава 6. Общие библиотечные интерфейсы - часть 1 struct passwd *getpwuid(uid_t uid) Сходным образом ищет пользователя с номером ID, приведенным в uid, возвращая указатель на static struct passwd, описывающий пользователя, или NULL, если пользователь не найден. getpwuid () - вот что нужно, когда«сть номер ID пользователя (такой, как в struct stat) и вам нужно вывести имя соответствующего пользователя. getpwnamO преобразует имя в номер ID пользователя, например, если вы хотите использовать с файлом chown () или f chown (). Теоретически обе эти процедуры осуществляют линейный поиск по базе данных паролей для обнаружения нужных сведений. На практике это верно, когда используется файл паролей; однако, кулуарные базы данных (сетевые или другие, как на системах BSD) склоняются к использованию более эффективных методов хранения, так что эти вызовы, возможно, в таком случае не такие дорогие6. getpwent () полезна, когда нужно пройти через всю базу данных паролей. Например, может быть необходимо прочесть ее всю в память, отсортировать, а затем осуществить быстрый поиск с помощью bsearch (). Это очень полезно для избежания множества линейных поисков, свойственных поиску по одному элементу за раз с помощью getpwuid () или getpwnam(). ^ЗАМЕЧАНИЕ. • Указатели, возвращаемые getpwent (), getpwnamO \и getpwuid(), все указывают на внутренние static данные. Поэтому следует I сделать копию их содержимого, если нужно сохранить сведения. \Хорошенько рассмотрите определение struct passwd. Члены, представляющие \ символьные строки, являются указателями; они также указывают на внутренние I static данные, и если вы собираетесь скопировать структуру, не забудьте также \скопировать и данные, на которые указывает каждый член структуры. 6.3.2. База данных групп Формат базы данных групп /etc/group подобен формату /etc/passwd, но с меньшим числом полежи: $ grep arnold /etc/group mail:x:12:mail,postfix,arnold uucp:x:14:uucp,arnold floppy:x:19:arnold devel:x:42:miriam,arnold arnold:x:2076:arnold Опять-таки на одну группу отводится одна строка, с полями, разделенными двоеточием. Поля следующие: К сожалению, если производительность является проблемой, нет стандартных способов узнать, как ваша библиотека осуществляет работу, а на самом деле способ ее работы может варьировать во время исполнения! (См. справочную страницу nsswitch.conflb) в системе GNU/Linux.) С другой стороны, назначением API помимо всего прочего является сокрытие деталей. - Примеч. автора.
6.3. Имена пользователей и групп 199 Имя группы Это имя группы, как оно отображается в' 1 s -1'илив любом другом контексте, когда требуется имя группы. Пароль группы Историческое поле. Оно больше не используется. ID группы Как и для ID пользователя, должен быть уникальным для каждой группы. Список пользователей Разделенный запятыми список пользователей, являющихся членами группы. В предыдущем примере мы видели, что пользователь arnold является членом нескольких групп. Это членство на практике отражается в том, что называют набором групп (group set). Помимо главных номеров ID пользователя и ID группы, которые есть у процессов, набор групп является набором номеров ID дополнительных групп, который имеет при себе каждый процесс. Система проверяет на соответствие с этими ГО групп, ID группы файла при осуществлении проверки прав доступа. Эта тема более подробно обсуждается в разделе 11 «Разрешения и ID пользователя и группы». API базы данных групп сходна с API для базы данных пользователей. Следующие функции определены в <grp. h>: #include <sys/types.h> /* XSI */ #include <grp.h> struct group *getgrent(void); void setgrent(void); void endgrent(void); struct group *getgrnam(const char *name); struct group *getgrgid(gid_t gid); struct group соответствует записям в /etc/group: struct group { char *gr__name; /* имя группы */ char *gr__passwd; /* пароль группы */ gid_t gr_gid; /* id группы */ char **gr_mem; /* члены группы */ }; Поле gr_mem требует некоторого объяснения. Хотя оно объявлено в виде указателя на указатель (char **), лучше представить его как массив строк (наподобие argv). Последний элемент в массиве устанавливается в NULL. Когда в списке нет членов, первый элемент массива равен NULL.
200 Глава 6. Общие библиотечные интерфейсы - часть 1 ch06-groupinfo. с демонстрирует, как использовать struct group и поле gr_mem. Программа принимает в командной строке имя единственного пользователя и печатает все записи групп, в которых появляется этот пользователь: I /* ch06-groupinfо.с Демонстрация getgrent() и struct group */ 2 3 #include <stdio.h> 4 #include <sys/types.h> 5 #include <grp.h> 6 7 extern void print_group(const struct group *gr); 8 9 /* main вывести строки групп для пользователя в argv[l] */ 10 II int 12 main(int argc, char **argv) 13 { 14 struct group *gr; 15 int i; 16 17 if (argc != 2) { /* Проверка аргументов */ 18 fprintf(stderr, "usage: %s user\n", argv[0]); 19 exit(l); 20 } 21 22 while ((gr = getgrent()) != NULL) /* Получить запись каждой группы */ 23 for (i = 0; gr->gr_mem[i] != NULL; i++) /* Рассмотреть каждый член */ 24 if (strcmp(gr->gr_mem[i], argv[l]) == 0) /* Если пользователь найден... */ 25 print_group(gr); /* Вывести запись */ 26 27 endgrent( ) ; 28 29 exit@); 30 } Функция main () сначала проверяет ошибки (строки 17-20). Основным компонентом программы является вложенный цикл. Внешний цикл (строка 22) перечисляет все записи базы данных группы. Внутренний цикл (строка 23) перечисляет всех членов массива gr_mem. Если один из членов соответствует имени из командной строки (строка 24), для печати записи вызывается print_group () (строка 25): 32 /* print_group печать записи группы */ 33 34 void 35 print_^group (const struct group *gr) 36 {
6.4. Терминалы: isatty () 201 37 int i; 38 39 printf("%s:%s:%ld: ", gr->gr__name, gr->gr_passwd, (long) gr->gr_gid); 40 41 for (i = 0; gr->gr_mem[i] != NULL; i++) { 42 printf ("%s" , gr->gr_mem[i]); 43 if (gr->gr_mem[i+l] != NULL) 44 putchar(','); 45 } 46 47 putchar('\n'); 48 } Функция print_group () (строки 34-48) проста, ее логика подобна логике main () для печати списка членов. Члены списка группы разделены запятыми; поэтому тело цикла до вывода запятой должно проверить, что следующий элемент в массиве не является NULL. Этот код работает правильно, даже если в группе нет членов. Однако мы знаем, что для этой программы есть члены, иначе pr in t_gr oup () не была бы вызвана! Вот что происходит при запуске программы: $ ch06-groupinfo arnold mail:x:12:mail,postfix,arnold uucp:x:14:uucp,arnold floppy:x:19:arnold devel:x:42:miriam,arnold arnold:x:2076:arnold 6.4. Терминалы: isatty о Модель стандартного ввода, стандартного вывода и стандартной ошибки Linux/Unix препятствует специальной трактовке устройств ввода и вывода. Программам обычно не нужно знать или беспокоиться о том, направляется ли их вывод на терминал, в файл, канал, физическое устройство или что-то еще. Однако иногда бывают моменты, когда программе действительно нужно знать, с какого рода файлом связан файловый дескриптор. Семейство вызовов stat () часто предоставляет достаточно сведений: обычный файл, каталог, устройство и т. д. Хотя иногда даже этого недостаточно, и для интерактивных программ, в частности, вам может потребоваться знать, не представляет ли дескриптор файла tty. tty (сокращение для Teletype, одного из ранних производителей компьютерных терминалов) является любым устройством, представляющим терминал, т. е. нечто, что человек мог бы использовать для взаимодействия с компьютером. Это может быть либо аппаратное устройство, такое, как клавиатура и монитор персонального компьютера, или старинный терминал видеодисплея, соединенный с компьютером через последовательный порт
202 Глава 6. Общие библиотечные интерфейсы - часть 1 или модем, или программный псевдотерминал, такой, который используется в оконных системах и при сетевых регистрациях. Различить можно с помощью isatty (): #include <unistd.h> /* POSIX */ int isatty(in.t desc); Эта функция возвращает 1, если дескриптор файла desc представляет терминал, в противном случае 0. В соответствии с POSIX isatty () может установить errno для указания ошибки; поэтому до вызова isatty () следует установить errno в 0, а затем проверить ее значение, если был возвращен 0. (Справочная страница GNU/Linux isattyC) не упоминает об использовании errno.) Стандарт POSIX также указывает, что просто возврат isatty () 1 не означает, что на другом конце дескриптора файла находится человек! Одним местом, где используется isatty(), является современная версия Is, в которой имена файлов по умолчанию печатаются в столбцы,, если терминалом является стандартный вывод, а если нет, они печатаются по одной на строчку. 6.5. Рекомендуемая литература 1. Mastering Algorithms With С by Kyle Loudon. O'Reilly & Associates, Sebastopol, California, USA, 1999. ISBN! 1-56592-453-3. Эта книга предоставляет практическое, утилитарное введение в алгоритмы и структуры данных с использованием С, освещая среди прочих вещей таблицы хэшей, деревья, сортировку и поис^. 2. The Art of Computer Programming Volume 3: Sorting and Searching, 2nd edition, by Donald E. Knuth. Addison-Wesley, Reading Massachusetts, USA, 1998. ISBN: 0-201-89685-0.7 На эту книгу обычно ссылаются как на последнее слово в сортировке и поиске. Примите во внимание, что она значительно более сжата и труднее для чтения, чем книга Loudon'a. 3. Проект GTK+8 состоит из нескольких совместно работающих библиотек. GTK+ является лежащим в основе инструментарием, используемым проектом GNU GNOME9. В основе иерархии библиотек располагается Glib, библиотека фундаментальных типов, структур данных и функций для работы с ними. Glib включает возможности для всех основных операций, которые мы до сих пор рассмотрели в данной книге, и многое другое, включая связанные списки и хэш-таблицы. Для просмотра онлайн-документов начните с веб-сайта проекта документации GTK+ , щелкните на ссылке «Загрузить» (Download) и идите дальше по онлайн-версии. 7 Русский перевод: Дональд Е. Кнут. Искусство программирования. Том 3. Сортировка и поиск B-е издание). Москва - Санкт-Петербург - Киев, Вильяме, 2000. - Примеч. науч. рей. ht tp: / /www. gtk .org - Примеч. автора. 9 ht tp : / /www. gnome. org - Примеч. автора. ht tp: / /www. gtk. org/ rdp - Примеч. автора.
6.6. Резюме 6.6. Резюме Время внутренне хранится в виде значений time_t, представляющих «секунды с начала Эпохи». Эпоха для систем GNU/Linux и Unix начинается с полночи 1 января 1970 г. по UTC. Текущее время получается от системы с помощью системного вызова time О, a dif f time () возвращает разницу в секундах между двумя значениями time_t. • Структура struct tm представляет «разложенное время», которое является значительно более удобным представлением даты и времени. gmtimeO и localtimeO преобразуют значения time_t в значения struct tm, a mktimeO действует в обратном направлении. • asctime() и ctime() осуществляют упрощенное форматирование значений времени, возвращая указатель на static строку символов фиксированного размера и формата. strftime() предусматривает гораздо более гибкое форматирование, включая значения на основе местных настроек. • Сведения о часовом поясе доступны через вызов tzset (). Поскольку стандартные процедуры действуют так, как если бы они автоматически вызывали tzset (), необходимость в непосредственном вызове этой функции возникает редко. • Стандартной процедурой для сортировки массивов является qsort (). Используя предоставленную пользователем функцию сравнения и принимая параметры числа элементов массива и их размера, qsort () может сортировать любые виды данных. Это обеспечивает значительную гибкость. ^ • scandir () читает в массив struct dirent каталог целиком. Для выбора того, какие элементы включить в массив и для обеспечения упорядочения элементов в массиве могут использоваться предоставленные пользователем функции, alphas or t () является стандартной функцией для сортировки элементов каталога по имени; scandir () передает функцию сортировки прямо через qsort (). • Функция bsearch() работает подобно qsort (). Она осуществляет быстрый бинарный поиск. Используйте ее, если цена линейного поиска перевешивает цену сортировки ваших данных. (Дополнительный API для поиска коллекций данных описан в разделе 14.4 «Расширенный поиск с помощью двоичных деревьев».) Базы данных пользователей и групп могут храниться в файлах на локальном диске или могут быть доступны через сеть. Стандартный API намеренно скрывает это различие. Каждая база данных обеспечивает как линейный просмотр всей базы данных, так и непосредственные запросы имени или ID пользователя/группы. • Наконец, для тех случаев, когда недостаточно простого stat (), isatty () может вам сообщить, представляет ли открытый файл устройство терминала. Упражнения 1. Напишите простую версию команды date, которая принимает в командной строке строку формата и использует ее для форматирования и вывода текущего времени. 2. Когда файл старше шести месяцев, 'Is -1' использует для печати времени изменения более простой формат. GNU версия файла Is . с использует следующее вычисление: 3 043 /* Время считается недавним, если оно в пределах последних б 3044 месяцев. В Григорианском годе 365.2425 * 24 * 60 * 60 ==
204 Глава 6. Общие библиотечные интерфейсы - часть 1 3045 31556952 секунд в.среднем. Запишите это значение как 3046 целую константу для избежания трудностей с плавающей точкой.*/ 3047 six_months_ago = current_time - 31556952 /2; Сравните это с нашим примером вычисления шести прошлых месяцев. Каковы преимущества и недостатки каждого из методов? 3. Напишите простую версию команды touch, которая изменяет время модификации файла, имя которого указано в командной строке, на текущее время. 4. Добавьте к вашей команде touch опцию, которая принимает в командной строке значения даты и времени и использует их в качестве нового времени модификации файлов, указанных в командной строке. 5. Добавьте к своей версии touch еще одну опцию, которая принимает имя файла и использует время модификации данного файла как новое время модификации файла, указанного в командной строке. 6. Усовершенствуйте ch06-sortemp. с так, чтобы она сортировала отдельный массив указателей, указывающих на массив сотрудников. 7. Добавьте к chO6-sortdir. с опции для сортировки по номеру индекса, времени модификации, времени доступа и размеру. Добавьте «обратную опцию», так, чтобы основанная на времени сортировка первым помещала самый недавний файл, а по другим критериям (размеру, индексу) помещала вначале наибольшее значение. 8. Напишите простую версию команды chown. Она должна использоваться так: chown пользователь[:группа] файлы ... Здесь пользователь и группа являются именами пользователя и группы, представляющими новых пользователя и группу для указанных файлов. Группа необязательна; если она присутствует, она отделяется от пользователя двоеточием. Чтобы протестировать свою версию на системе GNU/Linux, вы должны зарегистрироваться в качестве root. Делайте это осторожно! 9. Усовершенствуйте свою chown, чтобы допустить использование числовых значений пользователя или группы наряду с их именами. 10. Напишите функции для копирования структур пользователя и группы, включая указываемые данные. Для выделения памяти используйте при необходимости mall ос (). 11. Напишите специализированную библиотеку поиска пользователей, которая считывает в динамически выделяемый массив всю базу данных пользователей. Предусмотрите быстрый поиск пользователей как по ID, так и по именам. Гарантируйте обработку случая, при котором запрошенный пользователь не найден. 12. Сделайте то же самое для базы данных групп. 13. Напишите программу stat, которая печатает содержимое struct stat для каждого файла, указанного в командной строке. Она должна выводить все значения в формате, удобном для восприятия человеком: значения time_t в виде дат и времени, значения uid_t и gid_t в виде соответствующих имен (если они доступны), а также содержимое символических ссылок. Выведите поле st_mode таким же образом, как вывела бы Is. Сравните свою программу с программой stat GNU Coreutils как по их. выводу, так и по исходному коду.
Глава 7 Соединяя все вместе: is В этой главе: • 7.1. Опции V7 Is 205 • 7.2. Код V7 Is 206 • 7.3. Резюме 222 • Упражнения 223 Жлхманда V7 Is хорошо связывает воедино все, что мы до сих пор видели. Она использует почти все API, которые мы рассмотрели, затрагивая многие аспекты программирования Unix: выделение памяти, вспомогательные данные файлов, времена и даты, имена пользователей, чтение каталогов и сортировку. 7.1. Опции V7 Is По сравнению с современными версиями Is, V7 Is принимает лишь небольшое количество опций, а значение некоторых из них для V7 отличается от значения для современной Is. Эти опции следующие: -а Выводит все элементы каталога. Без нее '.' и '. .' не выводятся. Довольно интересно, V7 игнорирует лишь '.' и '. .', тогда как с VI по V6 игнорируется любой файл, имя которого начинается с точки. Это последнее является также поведением по умолчанию и для современных версий Is. -ft Вместо времени модификации файла использует для -t или -1 время изменения индекса. -d Для аргументов каталогов выводит сведения о самом каталоге, а не о его содержимом. -f «Заставляет» читать каждый элемент как каталог и печатать найденное в каждом слоте имя. Эта опция отключает -1, -г, -s, -t и включает -а. (Эта опция, очевидно, существует для отладки и исправления файловой системы.) -д Для 'Is -1' использует вместо имени пользователя имя группы. -i Выводит в первом столбце номер индекса вместе с именем файла или длинным листингом. -1 Осуществляет привычный вывод в длинном формате. Обратите, однако, внимание, что V7 4 Is -1' выводила лишь имя владельца, а не имена владельца и группы вместе.
206 Глава 7. Соединяя все вместе: 1 s -г Изменяет порядок сортировки, будь то по алфавиту для имен файлов или по времени. -s Выводит размер файла в 512-байтовых блоках. Справочная страница V7 &A) утверждает, что вспомогательные блоки (indirect blocks) - блоки, используемые файловой системой для обнаружения блоков больших файлов - также учитываются при вычислении, но, как мы увидим, это утверждение было неверным. -1 Сортирует вывод вместо имени по времени модификации, сначала более ранние. -и С опциями -t и/или -1 использует время доступа вместо времени модификации. Наибольшие различия между V7 Is и современной Is затрагивают опцию -а и опцию -1. Современные системы опускают все файлы с точками, если н^ указана -а, и они включают в длинный листинг -1 имена и владельца, и группы. На современных системах -д означает вывод лишь имени группы, а -о означает вывод лишь имени владельца. Стоит заметить, что у GNU Is свыше 50 опций! У 7.2. Код V7 Is Файл /usr/src/cmd/ls .с в дистрибутиве V7 содержит код. Весь он занимает 425 строк. 1 /*• 2 * перечисляет файлы или каталоги 3 */ 4 5 #include <sys/param.h> 6 #include <sys/stat.h> i 7 #include <sys/dir.h> 8 #include <stdio:h> 9 10 #define NFILES 1024 11 FILE *pwdf, *dirf; 12 char stdbuf[BUFSIZ] ; 13 14 struct lbuf { /.* Собирает необходимые сведения */ 15 union { 16 char lname[15]; 17 char *namep; 18 } In; 19 char ltype; 2 0 short lnum; 21 short Iflags; 22 short lnl; 23 short luid; 24 short lgid;
7.2. Код Wis 207 25 long lsize; 26 long lmtime; 27 }; 28 29 int aflg, dflg, lflg, sflg, tflg, uflg, iflg, fflg, gflg, cflg; 30 int rflg = 1; 31 long year; /* Глобальные'переменные: инициализируются 0 */ 32 int flags; 33 int lastuid = -1; 34 char tbuf[16]; 3 5 long tblocks; 36 int statreq; 37 struct lbuf *flist[NFILES]; 38 struct lbuf **lastp = flist; 39 struct lbuf **firstp = flist; 40 char *dotp = "."; 41 42 char *makename(); /* char *makename(char *dir, char *file); */ 43 struct lbuf *gstat(); /* struct lbuf *gstat(char *file,• int argfl); */ 44 char *ctime(); /* char *ctime(tdme_t *t); */ 45 long nblock(); /* long nblockdong size); */ 46 47 Jdefine ISARG 0100000 Программа начинается с включения файлов (строки 5-8) и объявлений переменных, struct lbuf (строки 14-27) инкапсулирует части struct stat, которые интересны Is. Позже мы увидим, как эта структура'заполняется. Переменные aflg, dflg и т. д. (строки 29 и 30) все указывают на наличие соответствующей опции. Такой стиль именования переменных типичен для кода V7. Переменные flist, lastp и firstp (строки 37-39) представляют файлы, о которых Is выводит сведения. Обратите внимание, что flist является массивом фиксированного размера, которая позволяет обрабатывать не более 1024 файлов. Вскоре мы увидим, как используются все эти переменные. После объявлений переменных идут объявления функций (строки 42-45), а затем определение ISARG, которая различает файл, указанный в командной строке, от файла, найденного при чтении каталога. 49 main(argc, argv) /* int main(int argc, char **argv) */ 50 char *argv[N] ; 51 { 52 int i; 53 register struct lbuf *ep, **epl; /* Объявления переменных и функций */ 54 register struct lbuf **slastp; 55 struct lbuf **epp;
208 Глава 7. Соединяя все вместе: 1 s 56 57 58 59 60 61 62 struct lbuf lb; char *t; int compar(); setbuf(stdout, stdbuf); time(&lb.lmtime); /* Получить текущее время */ year = lb.lmtime - 6L*30L*24L*60L*60L; /* 6 месяцев назад */ Функция main () начинается с объявления переменных и функций (строки 52-58), устанавливая буфер для стандартного вывода, получая время дня (строки 60-61) и вычисляя значение секунд с начала Эпохи для примерно шести месяцев (строка 62). Обратите внимание, что у всех констант есть суффикс L, указывающий на использование арифметики long. 63 if (--argc > 0 && *argv[lj ==¦»-¦){ argv) switch (**argv) { /* Разбор опций */ 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 argv++; while (*++*a case 'a': /* aflg++; continue; case 's': /* sflg++; statreq++; continue; case 'd': /* dflg++; continue; case 'g': /* gflg++; continue; case '1¦: /* lflg++; statreq++; continue; casQ 'r': /* rflg = -1; continue; case 't': /* tflg++; Все элементы каталога */ Размер в блоках */ Сведения о каталоге, не содержание */ Имя группы вместо имени владельца */ Расширенный листинг */ Обратный порядок сортировки */ Сортировка по времени-, не по имени */
7.2. Код Wis 209 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 statreq++; continue; case 'u': /* uflg++; continue; case 'c': /* cflg++; continue; case 'i': /* iflg++; continue; case 'f': /* fflg++; continue; default: /* continue; } argc--; } Время доступа, а не изменения */ Время изменения индекса, а не файла */ Включить номер индекса */ Форсировать чтение каждого arg как каталога */ Незнакомые буквы опций игнорировать */ Строки 63-118 разбирают опции командной строки. Обратите внимание на ручной разбор кода: getopt () еще не была придумана. Переменная statreq устанавливается в true, когда опция требует использования системного вызова stat (). Избежание ненужного вызова stat () для каждого файла дает большой выигрыш в производительности. Вызов call() был чрезвычайно дорогим, поскольку он мог вызвать поиск расположения индекса на файле, дисковое чтение для получения индекса, а затем поиск на диске расположения содержимого каталога (для того, чтобы продолжить чтение элементов каталога). В современных системах индексы находятся в группах, распределенных по всей файловой системе, вместо объединения их вместе в начале. Это дает заметный прирост производительности. Тем не менее, вызовы stat () до сих пор не бесплатны; вы должны использовать их лишь при необходимости, но не более. 119 if (fflg) .{ /* -f аннулирует -1, -s, -t, добавляя -а */ 120 121 122 123 124 aflg++; lflg = 0; sflg = 0; tflg = 0; statreq = 0
210 Глава 7. Соединяя все вместе: Is 125 } 126 if(lflg) { /* Открыть файл паролей или групп */ 127 t = "/etc/passwd"; 128 if(gflg) 129 t = "/etc/group"; 13 0 pwdf = fopen(t, "r"); 131 } 132 if (argc==0) { /* Если нет аргументов, использовать.текущий */ 133 argc++; 134 argv = &dotp - 1; 135 } Строки 119-125 обрабатывают опцию -f, выключая -1, -s, -t и statreq. Строки 126-131 обрабатывают -1, устанавливая для файла чтение сведений о владельце или группе. Помните, что V7 показывает лишь одно из этих сведений, но не оба. Если аргументов больше не осталось, строки 132-135 устанавливают argv таким образом, что он указывает на строку, представляющую текущий каталог. Назначение 'argr = &dotp - 1' действительно, хотя и необычно. ' - V компенсирует Ч+argv' в строке 137. Это позволяет избежать в главной части программы специального случая для 'argc == 1'. 136 for (i=0; i < argc; i++) { /* Получить сведения о каждом файле */ 137 if ((ер = gstat(*++argv, 1))==NULL) 13 8 continue; 139 ep->ln.namep = *argv; 140 ep->lflags |= ISARG; 141 } 142 qsort(firstp, lastp - firstp, sizeof *lastp, compar); 143 slastp = lastp; 144 for (epp=firstp; epp<slastp; epp++) { /* Глав, код, см. текст */ 145 ер = *ерр; 146 if (ep->ltype=='d' &&dflg==0 || fflg) { 147 if (argol) 148 printf("\n%s:\n", ep->ln.namep); 149 lastp = slastp; 150 readdir(ep->ln.namep); 151 if (fflg==0) 152 qsort(slastp,lastp - slastp,sizeof *lastp,compar); 153 if (lflg || sflg) 154 printf("total %D\n", tblocks); 155 for (epl = slastp; epl<lastp; epl-f-+) 156 pentry(*epl); 157 } else 158 pentry(ep); 159 }
7.2. Код Wis 211 160 exit@); 161 } /* Конец main() */ Строки 136-141 перебирают аргументы, собирая сведения о каждом. Второй аргумент gstat () булевый: true, если имя является аргументом командной строки, в противном случае false. Строка 140 добавляет флаг ISARG к полю If lags для каждого аргумента командной строки. Функция gstat (.) добавляет каждую новую struct lbuf к глобальному массиву f list (строка 137). Она также обновляет глобальный указатель lastp, чтобы он указывал в этом массиве на текущий последний элемент. Строки 142-143 сортируют массив, используя qsort (), и сохраняют текущее значение lastp в slastp. Строки 144-159 перебирают в цикле каждый элемент массива, выводя соответствующим образом сведения о файле или каталоге. Код для каталогов заслуживает дальнейшего объяснения: if (ep->ltype=='d' && dflg==0 || fflg) ... Строка 146. Если файл является каталогом и -d не предусмотрено или было установлено -f, Is должна прочесть каталог вместо того, чтобы выводить сведения о самом каталоге, if (argol) printf (" \n%s : \n", ep->ln.namep) Строки 147-148. Выводят имя каталога и двоеточие, если в командной строке было указано несколько файлов. lastp = slastp; readdir(ep->ln.namep) Строки 149-150. Восстанавливают lastp из slastp. Массив f list действует как двухуровневый стек имен файлов. Аргументы командной строки хранятся с f irstp до slastp - 1. Когда readdir () читает каталог, она помещает структуры struct lbuf для содержимого каталога в стек, начиная с slastp и до lastp. Это показано на рис. 7.1. firstp slastp lastp struct lbuf * struct lbuf * struct lbuf * struct lbuf * struct [ lbuf *' [ struct lbuf * Массив flist т&зж&^ттжж^л<т?$?%? Из командной строки- Из readdir() Рис. 7.1. Массив flist как двухуровневый стек if (fflg==0) qsort(slastp,lastp - slastp,sizeof *lastp,compar) Строки 151-152. Сортируют элементы подкаталога, если не действует -f. if (lflg || sflg). printf ("total %D\n", tblocks) Строки 153-154. Выводят для -1 или -s общее число блоков, используемых файлами в каталоге. Эта сумма хранится в переменной tblocks, которая сбрасывается для каждого ката-
s 212 Глава 7. Соединяя все вместе: 1 s лога. На современных системах форматирующая строка %D для printf () эквивалентна %ld; она означает «вывести длинное целое». (В V7 есть также %ld, см. строку 192.) for (epl=slastp; epl<lastp; epl++) pentry(*epl) Строки 155-156. Выводит сведения о каждом файле в подкаталоге. Обратите внимание, что V7 Is спускается лишь на один уровень в дереве каталогов. У нее отсутствует современная «рекурсивная» опция -R. /* void pentry(struct lbuf *ap) */ /* He использующийся исторический артефакт из V6 Is */ 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 pentry (ар) struct lbuf *ap; { struct { char dminor, dmajor;}; register t; register struct lbuf *p register char **cp; p = ap; if (p->lnum == -1) return; if (iflg) printf("%5u ", p->lnui if (sflg) printf("%4D ", nblock(p- /* Номер индекса */ size)); /* Размер в блоках */ Процедура pentry () выводит сведения о файле. Строки 172-173 проверяют, установлен ли -1 в поле lnum, и если так, функция возвращается. Когда верно 'p->lnum == -1', структура struct lbuf недействительна. В противном случае это поле содержит номер индекса файла. Строки 174-175 выводят номер индекса, если действует -i. Строки 176-177 выводят общее число блоков, если действует -s. (Как мы увидим ниже, это число может быть неточным.) 17 8 if (lflg) { /* Расширенный листинг: */ 179 putchar(p->ltype); /* - Тип файла */ 180 pmode(p->lflags); /* - Права доступа */ 181 printf("%2d ", p->lnl); /* - Число ссылок */ 182 t = p->luid; 183 if(gflg) 184 t = p->lgid; 185 if (getname(t/ tbuf)==0) 186 printf("%-6.6s", tbuf); /* - Владелец или группа */ 187 else 188 printf("%-6d" , t); 189 if (p->ltype=='b' || p->ltype=='c') /* - Устройство: старший и младший номэра */ 190 printf (,,%3d/%3d", major ( (int)p->lsize) , minor ( (int)p->lsize) ) ; 191 else
7.2. Код Wis 213 192 printf("%71d", p->lsize); /* - Размер в байтах */ 193 cp = ctime(&p->lmtime); 194 if (p->lmtime < year) /* - Время изменения */ 195 'printf(" %-7.7s %-4.4s ", cp+4, cp+20); else 196 printf(" %-12.12s " , cp+4); 197 } 198 if (p->lflags&ISARG) 7* - Имя файла*/ ] 199 printf("%s\n", p->ln.namep); 200 else 201 printf("%.14s\n", p->ln.lname); 202 } Строки 178-197 обрабатывают опцию -1. Строки 179-181 выводят тип файла, права доступа и число ссылок. Строки 182-184 устанавливают t на ID владельца или группы, в зависимости от опции -д. Строки 185-188 получают соответствующее имя и выводят его, если оно доступно. В противном случае программа выводит числовое значение. Строки 189-192 проверяют, является ли файл блочным или символьным устройством. Если да, они выводят старшее и младшее номера устройств, извлеченные с помощью макросов ma j or () и minor (). В противном случае они выводят размер файла. Строки 193-196 выводят соответствующее время. Если оно старше шести месяцев, код выводит месяц, день и год. В противном случае, выводятся месяц, день и время (формат результата ctime () см. раздел 6.1.3.1 «Простое форматирование времени: asctime() и ctime О»). Наконец, строки 198-201 выводят имя файла. Мы знаем, что для аргумента командной строки это завершающаяся нулем строка, и может быть использована %s. Для файла, прочитанного из каталога, оно может не завершаться нулем, поэтому должна использоваться явно указанная точность, % . 14s. 204 getname(uid, buf) /* int getname(int uid, char buf[]) */ 205 int uid; Простое кэширование, см. текст Проверка безопасности */ Начать с начала файла */ 206 207 208 209 210 211 212 213 214 215 216 217 218 219 char buf[]; { int j , с, n, i; if (uid==lastuid) return@); if(pwdf == NULL) return(-1); rewind(pwdf); lastuid ='-1; do { i = 0; j = 0; n = 0; / / / / / / Индекс в массиве buf */ Число полей в строке */ Преобразование числового значения
214 Глава 7. Соединяя все вместе: 1 s while((c=fgetc(pwdf)) != ' if (c==EOF) return(-1); if (c==': ') { j++; с = ' 0 • ; } if (j==0) buf[.i++] = cj if (з==2) n = n*10 + с } } while (n != uid)t buf[i++.] = ' \0'; lastuid = uid; return@); /* /* /* - • г Число \ Первое Третье • 0 ' ; \n') \ г полей*/ поле поле k Продолжать - - /* г имя 1роче< */ числовой до обь шруж» 220 while((c=fgetc(pwdf)) != '\n') { /* Прочесть строки */ 221 222 223 224 225 226 227 228 229 if (j==2) /* Третье поле - числовой ID 230 231 232 } while (n != uid); /* Продолжать до обнаружения ID */ 233 234 235 236 } Функция getname () преобразует ID владельца или группы в соответствующее имя. Она реализует простую схему кэширования; если переданное uid то же самое, которое находится в глобальной переменной lastuid, функция возвращает 0 (все нормально); буфер уже содержит имя (строки 210-211). lastuid инициализируется в -1 (строка 33), поэтому этот тест не проходит, когда getname () вызывается первый раз. pwdf уже открыт либо в /etc/passwd, либо в /etc/group (см. строки 126-130). Код здесь проверяет, что открытие было успешным, и если нет, возвращает -1 (строки 212-213). Удивительно, Is не использует getpwuid() или getgrgid(). Вместо этого она использует преимущество того факта, что формат /etc/passwd и /etc/group идентичен для трех первых полей (имя, пароль, числовой ID) и что оба используют в качестве разделителя двоеточие. Строки 216-232 реализуют линейный поиск по файлу, j содержит число обнаруженных до сих пор двоеточий: 0 для имени и 2 для ID. Таким образом, при сканировании строки она заполняет как имя, так и ID. Строки 233-235 завершают буфер name, устанавливают в глобальной lastuid последний найденный ID и возвращают 0 для обозначения успеха. 23 8 long /* long nblock(long size) */ 239 nblock(size) 240 long size; 241 { 242 return((size+511)>>9); 243 } Функция nblock () сообщает, сколько дисковых блоков использует файл. Это вычисление основано на размере файла, возвращенном stat (). Размер блока V7 равен 512 байтам - размер физического сектора диска.
7.2. Код Wis 215 Вычисление в строке 242 выглядит несколько устрашающим. с>>9' является сдвигом вправо на девять битов. Это осуществляет деление на 512 для получения числа блоков. (На раннем аппаратном обеспечении сдвиг вправо выполнялся гораздо быстрее деления.) Пока все хорошо. Теперь, файл даже размером в один байт все равно занимает целый дисковый блок. Однако, '1 / 512' дает ноль (целое деление срезает), что неверно. Это объясняет 4size+511\ Добавляя 511, этот код гарантирует, что сумма дает правильное число блоков при делении на 512. Это вычисление, однако, лишь приблизительное. У очень больших файлов есть также дополнительные блоки. Несмотря на заявление в справочной странице V7 fe(l)> данное вычисление не принимает в расчет дополнительные блоки. Более того, рассмотрите случай файла с большими дырами (созданными установкой указателя файла дальше конца файла с помощью lseek ()). Дыры не занимают дисковых блоков; однако, это не отражается в значении размера. Поэтому вычисления, выполненные nblock (), будучи обычно верными, могут давать результаты больше или меньше реальных. По этим причинам в struct stat 4.2 BSD были добавлены члены st_blocks, которые затем были приняты для System V и POSIX. 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 " 263 264 265 266 267 268 2 69 270 int ml[ int m2[ int шЗ[ int m4[ int m5[ int m6[ int m7[ int m8[ int m9[ S_IREAD>>0, S_IWRITE»0/ S_ISUID, ' s' S_IREAD>>3, S_IWRITE»3, S_ISGID, 's' S_IREAD>>6, S_IWRITE»6, S_ISVTX, 'f 'r ' , ¦w' - S_ 'r ' , •w' - S_ 1 r' , , •-• }; IEXEC»0/ '-' }; , '-¦ ); IEXEC»3, '-' }; )r '-' }; 'w\ '-' }; S_IEXEC»6/ }; int *m[] = { ml, m2, m3 , m4, m5, m6, m7, m8, m9}; pmode (af lag) { register int **mp; /* void pmode(int aflag) */ flags = aflag; for (mp = &m[0] select(*mp++) mp < &m[sizeof(m)/sizeof(m[0j)];) select(pairp) register int *pairp; /* void select(register int *pairp) */ { register int n;
216 Глава 7. Соединяя все вместе: 1 s 271 n = *pairp++; 272 while (--n>=0 && (flags&*pairp++)==0) 273 pairp++; 274 putchar(*pairp); 275 } Строки 245-275 выдают права доступа к файлу. Код компактен и довольно элегантен; он требует тщательного изучения. Строки 245-253: массивы с ml по т9 кодируют биты прав доступа для проверки вместе с соответствующими буквами для вывода. На каждую выводимую букву режима файла имеется один массив. Первый элемент каждого массива является числом пар (право доступа, буква), закодированных в данном конкретном массиве. Последний элемент является буквой, которая должна быть выведена в случае, если не найден ни один из битов прав доступа. Обратите также внимание, что права доступа обозначены как 'I_READ>>0', 'I_^READ>>3', 'I_READ>>6' и т. д. Отдельные константы для каждого бита (S__IRUSR, S_IRGRP и т. п.) не были еще придуманы. (См. табл. 4.5 в разделе 4.6.1 «Указание начальных прав доступа к файлу».) Строка 255: массив m указывает на каждый из массивов с ml no m9. Строки 257-264: функция pmode () сначала устанавливает глобальную переменную flags равной переданному параметру af lag. Затем она просматривает в цикле массив т, передавая каждый элемент функции select (). Переданный элемент представляет один из массивов с ml no m9. Строки 266-275: функция select () понимает структуру каждого из массивов с ml по m9. n является числом пар в массиве (первый элемент); его устанавливает строка 271. Строки 272-273 ищут биты прав доступа, проверяя установленную ранее в строке 261 глобальную переменную flags. Обратите внимание на использование оператора ++ как в проверке цикла, так и в теле цикла. Результатом является пропуск пары в массиве, если в flags не обнаружен бит доступа в первом элементе пары. Когда цикл завершается, либо бит разрешения был найден, в этом случае pairp указывает на второй элемент пары, являющийся нужным для вывода символом, либо он не был найден, в этом случае pairp указывает на символ по умолчанию. В любом случае, строка 274 выводит символ, на который указывает pairp. Последним стоящим внимания моментом является то, что на С символьные константы (такие как 'х') имеют тип int, а не char1. Поэтому проблем с помещением этих констант в массив целых нет; все работает правильно. 277 char * /* char *makename(char *dir, char *file) */ 27 8 makename(dir, file) 279 char *dir, *file; 280 { В C++ это по-другому: там символьные константы действительно имеют тип char. Это различие не влияет на данный конкретный код. - Примеч. автора.
7.2. Код Wis 217 281 282 283 284 285 286 287 288 289 290 291 292 293 2 94 295 } static char dfile[100]; register char *dp, *fp; register int i; dp = dfile; fp = dir; while (*fp) *dp++ = *fp++; *dp++ = '/'; fp = file; for (i = 0; i<DIRSIZ; i + +) *dp+ + = *fp++; *dp = 0; return(dfile); Строки 277-295 определяют функцию makename (). Ее работа заключается в соединении имени каталога с именем файла, разделенным символом косой черты, с образованием строки. Она осуществляет это в static буфере dfile. Обратите внимание, что dfile всего лишь 100 символов длиной и что проверка ошибок не выполняется. Сам код прост, он копирует по одному символу за раз. makename () используется функцией readdir (). 297 readdir(dir) /* void readdir(char Mir) */ 298 char *dir; 299 { 300 static struct direct dentry; 301 register int j; 302 register struct lbuf *ep; 303 304 if ((dirf = fopen(dir, "r")) == NULL) { 305 printf("%s unreadable\n", dir); 3 06 return; 307 } 308 tblocks' = 0; 309 for(;;) { 310 if (fread((char *)&dentry, sizeof(dentry), 1, dirf) != 1) 311 break; 312 if (dentry. d__ino= = 0 313 || aflg= = 0 ScSc dentry. d_name [0 ] == ' . ' && (dentry. d^name [ 1] =='\0 ' 314 || dentry.d_name[1]=='.' && dentry.d_name[2]=='\0')) 315 continue; 316 ep = gstat(makename(dir, dentry.d_name), 0); 317 if (ep==NULL) 318 continue;
218 Глава 7. Соединяя все вместе: Is 319 if (ep->lnum != -1) 320 ep->lnum = dentry.d_ino; 321 for (j=0; j<DIRSIZ; j++) 322 ep->ln.lname[j] = dentry.d_name[j]; 323 } 324 fclose(dirf); 325 } Строки 297-325 определяют функцию readdir (), чья работа заключается в чтении содержимого каталогов, указанных в командной строке. Строки 304-307 открывают каталог для чтения, завершая функцию, если f open () возвращает ошибку. Строка 308 инициализирует глобальную переменную tblocks нулем. Ранее (строки 153-154) это использовалось для вывода общего числа блоков, использованных файлами в каталоге. Строки 309-323 являются циклом, который читает элементы каталога и добавляет их к массиву flist. Строки 310-311 читают один элемент, выходя из цикла в конце файла. Строки 312-315 пропускают неинтересные элементы. Если номер индекса равен нулю, этот слот не используется. В противном случае, если не был указан -а и имя файла является '.' или '. .', оно пропускается. Строки 316-318 вызывают gstat () с полным именем файла и вторым аргументом, равным f asle, указывающим, что он не из командной строки, gstat () обновляет глобальный указатель lastp и массив flist. Возвращаемое значение NULL обозначает какую-нибудь разновидность ошибки. Строки 319-322 сохраняют номер индекса и имя в struct lbuf. Если ep->lnum возвращается из gstat () установленным в -1, это означает, что операция stat() с файлом завершилась неудачей. Наконец, строка 324 закрывает каталог. Следующая функция, gstat () (строки 327-398), является центральной функцией для получения и сохранения сведений о файле. 327 struct lbuf * /* struct lbuf *gstat(char *file, int argfl) */ 328 gstat(file, argfl) 329 char *file; 330 { 3 31 extern char *malloc(); 332 struct stat statb; 333 register struct lbuf *rep; 334 static int nomocore; 335 336 if (nomocore) /* Ранее была нехватка памяти */ 3 37 return(NULL); 338 rep = (struct lbuf *)malloc(sizeof(struct lbuf)); 3 39 if (rep==NULL) { 340 fprintf(stderr, "Is: out of memory\n"); 341 nomocore = 1;
7.2. Код V7 Is 219 342 return(NULL); 343 } 344 if (lastp >= &flist[NFILES]) { /* Проверить, не дано ли слишком много файлов */ •345 static int msg; 346 lastp--; 347 if (msg==0) { 348 fprintf(stderr, "Is: too many files\n"); 349 msg++; 350 } 351 } 3 52 *lastp++ = rep; /* Заполнить сведения */ 353 rep->lflags = 0; 354 rep->lnum = 0;* 355 rep->ltype = '-'; /* Тип файла по умолчанию */ Статическая переменная nomocore [важно] указывает, чтошаНос () при предыдущем вызове завершилась неудачей. Поскольку она статическая, она автоматически инициализируется 0 (т. е. false). Если на входе она равна true, gs tat () просто возвращает NULL. В противном случае, если malloc () завершается неудачей, Is выводит сообщение об ошибке, устанавливает в nomocore true и возвращает NULL (строки 334-343). Строки 344-351 гарантируют, что в массиве f list все еще остается место. Если нет, Is выдает сообщение (но лишь однажды; заметьте использование статической переменной msg), а затем повторно использует последний слот f list. Строка 352 заставляет слот lastp указывать на новую struct lbuf (rep). Это также обновляет lastp, который используется для сортировки в main() (строки 142 и 152). Строки 353-355 устанавливают значения по умолчанию для полей флагов, номеров индексов и типов в .struct lbuf. 356 if (argfl || statreq) { 357 if (stat(file, &statb)<0) { /* stat() завершилась неудачей */ 358 printf("%s not found\n", file); 359 statb.st_ino = -1; 360 statb.st_size = 0; 3 61 statb.st_mode = 0; 362 if (argfl) { 363 lastp--; 3 64 return@); 365 } 366 } 3 67 rep->lnum = statb. st_ino; /.* stat() OK, копировать сведения */ 368 rep->lsize = statb.st_size; 369 switch(statb.st_mode&S_IFMT) { 370 371 case S_IFDIR:
220 Глава 7. Соединяя все вместе: 1 s 372 rep->ltype = 'd'; 373 break; 374 375 case S_IFBLK: 376 rep->ltype = 'b*; 377 rep->lsize = statb.st_rdev; 378 break; 379 , 3 80 case S__IFCHR: 381 rep->ltype = 'c'; 382 rep->lsize = statb.st_rdev; 3 83 break; 3 84 } 385 rep->lflags = statb.st_mode & ~S_IFMT; 386 rep->luid = statb.st_uid; 387 rep->lgid = statb.st_gid; 388 rep->lnl = statb.st_nlink; 389 if(uflg) 390 rep->lmtime = statb.st^atime; 391 else if (cflg) 3 92 rep->lmtime = statb.st_ctime; 393 else 394 rep->lmtime = statb.st_mtime; 395 tblocks += nblock(statb.st_size); 396 } 397 return(rep); 398 } Строки 356-396 обрабатывают вызов stat ().. Если это аргумент командной строки или если statreq установлен в true благодаря опции, код заполняет struct lbuf следующим образом: Строки 357-366: вызывают stat (), при ее неудаче выводится сообщение об ошибке с установкой соответствующих значений, затем возвращается NULL (выраженный в виде 0). Строки 367-368: устанавливают в struct stat поля номера индекса и размера, если вызов stat () был успешным. Строки 369-384: обрабатывают особые случаи каталогов, блочных и символьных устройств. Во всех случаях код обновляет поле ltype. Для устройств значение lsize замещается значением st_rdev. • Строки 385-388: заполняются поля If lags, luid, lgid и lnl из соответствующих полей в struct stat. Строка 385 удаляет биты типа файла, оставляя 12 битов прав доступа (на чтение/запись/исполнение для владельца/группы/остальных, а также setuid, setgid и save-text).
7.2. Код Wis 221 • Строки 389-394: основываясь на опциях командной строки, использует одно из трех полей времени в struct stat для поля lmtime в struct Ibuf. Строка 395: обновляет глобальную переменную tblocks числом блоков в файле. 400 compar(ppl, рр2) /* int compar(struct lbuf **ppl, */ 401 struct lbuf **ppl, **pp2; /* struct lbuf **pp2) */ 402 { 403 register struct lbuf *pl, *p2; 404 405 pi = *ppl; 406 p2 = *pp2; 407 if (dflg==0) { 408 if (pl->lflags&ISARG &'& pl->ltype-='d') { 409 if (!(p2->lflags&ISARG && p2->ltype=='d')) 410 returnA); 411 } else { 412 i^ (p2->lflags&ISARG && p2->ltype=='d') 413 return(-1); 414 } 415 } 416 if (tflg) { 417 if (p2->lmtime == pl->lintime) 418 return@); 419 if (p2->lmtime > pl->lmtime) 420 return(rflg); 421 return(-rflg); 422 } 423 return(rflg * strcmp(pl->lflags&ISARG? pl->ln.namep: pl->ln.lname, 424 p2->lflags&ISARG? p2->ln.namep: p2->ln.lname)); 425 } Функция compar () сжата: в небольшом пространстве происходит многое. Первая вещь, которую следует запомнить, это смысл возвращаемого значения: отрицательное значение означает, что первый файл должен идти перед вторым, ноль означает, что файлы равны, а положительное значение означает, что второй файл должен идти перед первым. Следующая вещь, которую нужно понять, это то, что Is выводит содержимое каталогов после выведения сведений о файлах. Поэтому результат сортировки должен быть таким, чтобы все каталоги, указанные в командной строке, следовали за всеми файлами, указанными там же. Наконец, переменная rflg помогает реализовать опцию -г, которая меняет порядок сортировки. Она инициализируется 1 (строка 30). Если -г используется, rflg устанавливается в -1 (строки 89-91). Следующий псевдокод описывает логику compar (); номера строк на левой границе соответствуют номерам строк Is . с:
222 Глава 7. Соединяя все вместе: 1 s 407 if Is должна прочесть каталоги # dflg ¦== 0 408 if pi аргумент командной строки и pi каталог 409 if p2 не аргумент командной строки и не каталог 410 return 1 # первый идет после второго else перейти на тест времени 411 else- # pi не каталог командной строки 412 if p2 аргумент командной строки и каталог 413 return -1 # первый идет перед вторым else перейти на тест времени 416 if сортировка основана на времени # tflg равно true # сравнить времена: 417 if время р2 равно времени pi 418 return 0 419 if время р2 > времени pi 420 return значение rflg (положительное или отрицательное) # время р2 < времени pi 421 return противоположное rflg значение (положительное или отрицательное) 423 Умножить rflg на результат strcmp() 424 для двух имен и вернуть результат Аргументы stгетр () в строках 423-424 выглядят сбивающими с толку. В зависимости от того, было ли имя файла указано в командной строке или было прочитано из каталога, должны использоваться различные члены объединения In в struct lbuf. 7.3. Резюме • V7 Is является сравнительно небольшой программой, хотя она затрагивает многие фундаментальные аспекты программирования Unix: файловый ввод-вывод, вспомогательные данные файлов, содержание каталогов, пользователи и группы, значения времени и даты, сортировку и динамическое управление памятью. • Наиболее примечательным внешним различием между V7 Is и современной Is является трактовка опций -а и -1. У версии V7 значительно меньше опций, чем у современных версий; заметным недостатком является отсутствие рекурсивной опции -R. • Управление flist является чистым способом использования ограниченной памяти архитектуры PDP-11, предоставляя в то же время как можно больше сведений, struct lbuf хорошо извлекает нужные сведения из struct . stat; это значительно упрощает код. Код для вывода девяти битов доступа компактен и элегантен. Некоторые части Is используют удивительно маленькие лимиты, такие, как верхняя граница числа файлов в 1024 или размер буфера в makename () в 100.
Упражнения 223 Упражнения 1. Рассмотрите функцию getname (). Что случится, если запрошенный ID равен 256, а в /etc/passwd есть следующие две строки, в этом порядке: joe rxyzzy:2160:10:Joe User:/usr/joe:/bin/sh jane:zzyxx:216:12:Jane User:/usr/jane:/bin/sh 2. Рассмотрите функцию makename (). Может ли она использовать sprintf О для составления имени? Почему может или почему нет? 3. Являются ли строки 319-320 в readdir () действительно необходимыми? 4. Возьмите программу stat, которую вы написали в качестве упражнения в «Упражнениях» к главе 6. Добавьте функцию nblockf) из V7 Is и выведите результаты вместе с полем st_blocks из struct stat. Добавьте видимый маркер, когда они различны. 5. Как бы вы оценили V7 Is по ее использованию malloc () ? (Подсказка: как часто вызывается free () ? Где ее следовало бы вызвать?) 6. Как вы оценили бы ясность кода V7 Is? (Подсказка: сколько там комментариев?) 7. Очертите шаги, которые нужно было бы сделать, чтобы адаптировать V7 Is для современных систем.
Глава 8 Файловые системы и обходы каталогов В этой главе: 8.1. Монтирование и демонтирование файловых систем 224 8.2. Файлы для администрирования файловой системы 234 8.3. Получение сведений о файловой системе 240 8.4. Перемещение по иерархии файлов 252 8.5. Обход дерева файлов: GNU du 264 8.6. Изменение корневого каталога: chroot () 272 8.7. Резюме 273 Упражнения 274 Данная глава завершает обсуждение файловых систем и каталогов Linux (и Unix). Сначала мы опишем, как к логическому пространству имен файловой системы добавляется (и удаляется) раздел диска, содержащий файловую систему, таким образом, что в общем пользователю не нужно ни знать, ни заботиться о месте физического размещения файла, вместе с API для работы с файловыми системами. Затем мы опишем, как перемещаться по иерархическому пространству имен файлов, как получать полный путь текущего рабочего каталога и как без труда обрабатывать произвольные иерархии (деревья) каталогов, используя функцию nftw(). Наконец, мы опишем специализированный, но важный системный вызов chroot (). 8.1. Монтирование и демонтирование файловых систем Унифицированное иерархическое пространство имен файлов является большим достоинством дизайна Linux/Unix. Данный раздел рассматривает, как административные файлы, команды и операционная система объединяются для построения пространства имен из отдельных физических устройств, содержащих данные и служебные данные файлов. 8.1.1. Обзор основ В главе 5 «Каталоги и служебные данные файлов», были представлены индексы для служебных данных файлов и описано, как элементы каталогов связывают имена файлов с индексами. В ней также были описаны разделы и файловые системы, и вы видели, что прямые
8.1. Монтирование и демонтирование файловых систем 225 ссылки ограничены работой в пределах одной файловой системы, поскольку каталоги содержат лишь номера индексов, а последние не уникальны среди всего набора использующихся файловых систем. Помимо индексов и блоков данных, файловые системы содержат также одну или более копий суперблока. Это специальный дисковый блок, который описывает файловую систему; его сведения обновляются по мере изменений в самой файловой системе. Например, он содержит число свободных и используемых индексов, свободных и используемых блоков и другие сведения. Он включает также магическое число: специальное уникальное значение в специальном месте, которое идентифицирует тип файловой системы. (Вскоре мы увидим, насколько это важно.) Обеспечение доступа к разделу, содержащему файловую систему, называется монтированием (mounting) файловой системы. Удаление файловой системы из использования называется, что неудивительно, демонтированием (unmounting) файловой системы. Эти две задачи выполняются программами mount и umount [так], названными по соответствующим системным вызовам. У системного вызова mount () каждой системы Unix свой, отличный интерфейс. Поскольку монтирование и демонтирование считаются проблемой реализации, POSIX намеренно не стандартизует эти системные вызовы. Вы монтируете файловую систему в каталог; такой каталог называется точкой монтирования файловой системы. По соглашению, каталог должен быть пустым, но ничто не принуждает к этому. Однако, если точка монтирования не пуста, все ее содержимое становится полностью недоступным, пока в ней не смонтирована файловая система1. Ядро поддерживает уникальный номер, известный как номер устройства, который идентифицирует каждый смонтированный раздел. По этой причине именно пара (устройство, индекс) вместе уникально идентифицируют файл; когда структуры struct stat для двух имен файлов указывают, что оба эти номера одни и те же, можно быть уверенным, что они на самом деле ссылаются на один и тот же файл. Как упоминалось ранее, программы уровня пользователя помещают структуры индексов и другие вспомогательные данные на раздел диска, создавая тем самым файловую систему. Эти самые программы создают для файловой системы начальный корневой каталог. Таким образом, нам придется провести различие между «корневым.каталогом, названным /», который является каталогом самого верхнего уровня в иерархическом пространстве имен файлов, и «корневым каталогом файловой системы», который является отдельным каталогом верхнего уровня каждой файловой системы. Каталог / является также «корневым каталогом» «корневой файловой системы». * По причинам, описанным на врезке, у корневого каталога файловой системы номер индекса всегда равен 2 (хотя это не стандартизовано формально). Поскольку может быть несколько файловых систем, у каждой из них один и тот же номер индекса корневого ката- GNU/Linux и Solaris дают возможность монтировать один файл поверх другого; это продвинутое использование, которое мы не будем обсуждать. - Примеч. автора. 8-159
226 Глава 8. Файловые системы и обходы каталогов лога 2. При разрешении пути ядро знает, где смонтирована каждая файловая система, и заставляет имя точки монтирования ссылаться на корневой каталог смонтированной файловой системы. Более того, '. .' в корне смонтированной файловой системы ссылается на родительский каталог точки монтирования. На рис. 8.1 показаны две файловые системы: одна для корневого каталога, а другая для / usr, до того, как /usr смонтирована. На рис. 8.2 показана ситуация после монтирования /usr. v Корневая файловая система Dev. 302h/770d Inode: 2 Dev. 302h/770d Inotie: 2 Dev: 302h/770d Inode: 547288 Dev: 302h/770d Inode: 64385 Dev: 302h/770d Inode: 338017 bin Is sh Dev: 302h/770d Inode: 547288 Dev: 302h/770d Inode: 2 Dev: 302h/770d Inode: 547338 Dev: 302h/770d Inode: 547322 Dev. 302h/770d Dev: 302h/770d Inode: 57 Inode: 2 bin tmp share Dev: 305h/773d Inode: 2 Dev: 305h/773d Inode: 2 Dev: 305h/773d Inode: 354113 Dev: 305h/773d Inode: 338232 Dev: 305h/773d Inode: 450689 /dev/hdal Точка монтирования для /usr пуста Файловая система /usr /dev/hda3 Рис. 8.1. Отдельные файловые системы до монтирования Каталог /, корень всей логической иерархии, особый еще в одном отношении: /. и /.. ссылаются на один и тот же каталог; это неверно для любого другого каталога в системе. (Таким образом, после команды типа *cd /../../../..' вы все еще буцете в / .) Это поведение реализуется простым способом: как' /., так и /. . являются прямыми ссылками на корневой каталог файловой системы. (Вы можете видеть это как на рис. 8.1, так и 8.2.) Каждая файловая система работает таким способом, но ядро рассматривает/ особым образом и не рассматривает как особый случай каталог'. .' для файловой системы, смонтированной в /.
8.1. Монтирование и демонтирование файловых систем 227 Dev: 1 Dev: bin dev | usr ^ 1 Dev: | Dev: 1 Dev: 302h/770d 302h/770d 302h/770d 302h/770d 302h/770d Inode: Inode: Inode: Inode: Inode: 2 2 547288 64385 338017 Is sh Dev: 302h/770d Inode: 547288 Dev: 302h/770d Inode: 2 Dev: 302h/770d .Inode: 547338 Dev: 302h/770d Inode: 547322 Мттттттжштттжттттттж Dev: 305h/773d Dev: 302h/770d Dev: 305h/773d Dev: 305h/773d Dev: 305h/773d Точка-точка в корневой файловой системе Inode: 2 Inode: 2 Inode: 354113 Inode: 338232 Inode: 450689 Рис. 8.2. Отдельные файловые системы после монтирования Номера индексов корневого каталога Номер индекса для корневого каталога файловой системы всегда равен 2. Почему это так? Ответ имеет отношение как к технологии, так и к истории. Как упоминалось в разделе 5.3 «Чтение каталогов», элемент каталога с номером индекса ноль означает неиспользуемый, или пустой слот. Поэтому индекс 0 не может использоваться для настоящего файла или каталога. Хорошо, так что насчет индекса 1? Ну, особенно в 70-80 годах XX века, диски не были сделаны так же хорошо, как сейчас. Когда вы покупали диск, он приходил с (бумажным) списком испорченных блоков - известных мест на диске, которые не могли быть использованы. Каждой операционной системе приходилось отслеживать эти плохие блоки и избегать их использования. Под Unix это осуществлялось созданием файла особого назначения, блоки данных которого были известны, как испорченные. Этот файл присоединялся к индексу 1, оставляя 2 в качестве первого индекса, доступного для использования обычными файлами или каталогами. На современных дисках присутствует значительное количество встроенной электроники, и они рами управляют испорченными блоками. Поэтому технически было бы осуществимо использовать для файла индекс 1. Однако, поскольку такое большое количество программ Unix, которые предполагают, что индекс 2 является индексом для корневых каталогов файловых систем, Linux также следует этому соглашению. (Однако, Linux иногда использует индекс 1 для не собст- венных файловых систем, таких, как vf at или /proc.)
228 Глава 8. Файловые системы и обходы каталогов 8.1.2. Обзор различных типов файловых систем I ЗАМЕЧАНИЕ. Обсуждение в данном разделе специфично для Linux. Однако, у многих современных систем Unix также есть сходные особенности. Мы рекомендуем вам изучить документацию своей системы. Исторически V7 Unix поддерживал лишь один тип файловой системы; вспомогательные данные и организация каталогов каждого из разделов были структурированы одним и тем же способом. 4.1 BSD использовал файловую систему с такой же как у V7 структурой, но с размером блока 1024 байта вместо 512 байтов. 4.2 BSD ввело «файловую систему BSD», которая разительно изменила расположение индексов и данных на диске и дала возможность использовать гораздо большие размеры блоков. (В общем, использование больших протяженных блоков данных обеспечивает лучшую производительность, особенно для чтения файлов.) Вплоть до 4.3 BSD и System V Release 2 в начале и середине 1980-х системы Unix продолжали поддерживать один тип файловой системы. Для переключения компьютера от одной файловой системы на другую2 приходилось сначала резервировать каждую файловую систему на среду архивирования (9-дорожечную ленту), обновлять систему, а затем восстанавливать данные. В середине 1980-х Sun Microsystems разработала архитектуру ядра, которая сделала возможным использование нескольких архитектур файловой системы в одно и то же время. Этот проект был реализован в их операционной системе SunOS, сначала для поддержки сетевой файловой системы Sun (Network File System - NFS). Однако, как следствие, стало возможным также поддерживать несколько архитектур на диске. System V Release 3 использовала сходную архитектуру для поддержки удаленной файловой системы (Remote File System - RFS), но она продолжала поддерживать лишь одну архитектуру на диске3. (RFS никогда широко не использовалась и сейчас является лишь исторической сноской.) Общий дизайн Sun стал популярным и широко реализовывался в коммерческих системах Unix, включая System V Release 4. Системы Linux и BSD используют разновидность этого дизайна для поддержки множества форматов файловых систем на диске. В частности, обычным для всех разновидностей Unix на платформе Intel x86 является возможность монтирования файловых систем MS-DOS/Windows FAT, включая поддержку длинных имен, а также форматированные в соответствии с ISO 9660 CD-ROM. Linux имеет несколько собственных (т. е. размещаемых на диске) файловых систем. Наиболее популярными являются файловые системы ext2 и ext3. Однако, доступно значительно больше файловых систем. Сведения о большинстве из них вы можете найти в каталоге /usr/src/linux/Documentation/f ilesystems/ (если вы установили исход- 2 Например, при обновлении VAX 11/780 с 4.1 BSD до 4.2 BSD. - Примеч. автора. 3 System V Release 3 поддерживала два различных размера блоков: 512 байтов и 1024 байта, но в остальном организация диска была той же самой. - Примеч. автора.
8.1. Монтирование и демонтирование файловых систем 229 ный код ядра). В табл. 8.1 перечислены имена различных файловых систем с кратким описанием каждой из них. Сокращение «RW» означает «чтение/запись», a «RO» означает «только чтение». Таблица 8.1. Поддерживаемые ядром файловые системы Linux (ядро 2.4.x) Имя Режим Описание af s adf s af f s RW RW RO,RW autofs bef s bfs binfmt-misc ef s coda cramfs devf s devpts ext2 ext3 hfs hpf s intermezzo RW RO RW RW RW RW RO RW RW RW RW RW RW RW Andrew File System (файловая система Andrew) Acorn Advanced Disc Filing System (расширенная дисковая файловая система Acorn) Amiga Fast File system (быстрая файловая система Amiga). Режим «только для чтения» в противоположность режиму «для записи и чтения» зависит от версии файловой системы. Файловая система для взаимодействия с демоном автоматического монтирования. Файловая система BeOS. Помечена как программное обеспечение альфа. SCO UnixWare Boot Filesystem (загрузочная файловая система SCO Unix). Специальная файловая система для запуска интерпретаторов компилированных файлов (например, файлов Java). Файловая система, разработанная для варианта Unix SGI, названного Irix. Экспериментальная распределенная файловая система, разработанная в CMUa. Небольшая файловая система для хранения файлов в постоянной памяти (ROM). Способ динамического предоставления файлов для /dev (устарело). Специальная файловая система для псевдотерминалов. Вторая расширенная файловая система. Файловая система по умолчанию для GNU/Linux, хотя некоторые дистрибутивы используют теперь ext3. Файловая система ext2 с журналированием. Hierarchical File System (иерархическая файловая система) Apple Mac OS. High Performance File System (высокопроизводительная файловая система) OS/2. Экспериментальная распределенная файловая система для работы в отсоединенном от сети состоянии. См. веб-сайт InterMezzo(http: //www.inter~mezzo.org).
230 Глава 8. Файловые системы и обходы каталогов Таблица 8.1. Поддерживаемые ядром файловые системы Linux (ядро 2.4.x) (Продолжение) Имя Режим Описание jffs jffs2 iso9660 RW RW RO jfs ncp ntfs openpromfs proc qnx4 ramf s reiserfs romf s smbf s sysv tmpf s udf uf s urns do s usbf s RW RW RO RO RW RW RW RW RO RW RW RW RO RO,RW RW RW Jorunaled Flash Filesystem (журналируемая файловая система с групповой записью/считыванием, для встроенных систем). Journaled Flash Filesystem 2 (тоже для встроенных систем). Файловая система ISO 9660 для CD-ROM. Поддерживаются также расширения Rock Ridge, заставляющие выглядеть использующие их CD-ROM как нормальная файловая система (но только для чтения). Journaled File System (журналируемая файловая система) IBM для Linux. Протокол Novell NCP для NetWare; клиент удаленной файловой системы. Поддержка файловой системы NTFS Windows. Файловая система /ргос для PROM на системах SPARC. Доступ к информации о процессах и ядре. Файловая система QNX4 (небольшой операционной систе- *мы реального времени). Файловая система для создания RAM-дисков. Развитая журналируемая файловая система. Файловая система для создания простых RAM-дисков только для чтения. Поддержка клиента для файловых систем SMB (разделяемых файлов Windows). Файловые системы System V Release 2, Xenix, Minix и Coherent, coherent, minix и xenix являются псевдонимами. Файловая система электронного диска, поддерживающая динамический рост. Формат файловой системы UDF, используемый в DVD- ROM. Быстрая файловая система BSD; на современных системах с доступом для чтения и записи. Расширение vf at, заставляющее выглядеть ее подобно файловой системе Unix. Специальная файловая система для работы с устройствами USB. Первоначальным именем было usbdevf s, это имя до сих пор появляется, например, в выводе mount.
8.1. Монтирование и демонтирование файловых систем 231 Таблица 8.1. Поддерживаемые ядром файловые системы Linux (ядро 2.4.x) (Продолжение) Имя Режим Описание vf at RW Все варианты файловых систем FAT MS-DOS/Windows. Компонентами являются msdos и fat. vxfs RW Журналируемая файловая система Veritas VxFS. xfs RW Высокопроизводительная журналирующая файловая система, разработанная SGI для Linux. См. веб-сайт XFS (http://oss.sgi.com/projects/xfs/). а. Университет Карнеги-Меллона. - Примеч. перев. Не все из этих файловых систем поддерживаются командой mount; список поддерживаемых см. в mount(&). Журналирование является методикой, впервые использованной в системах баз данных для увеличения производительности обновлений файлов таким образом, что восстановление файловой системы в случае аварии могло быть сделано быстро и правильно. В момент написания этого были доступны несколько различных журналируемых файловых систем, конкурирующих за продвижение в мире GNU/Linux. Одной из них является ext3; у нее преимущество обратной совместимости с существующими файловыми системами ext2, очень просто конвертировать файловые системы туда-сюда между этими двумя видами. (См. tune2fs(%).) ReiserFS и XFS также имеют своих твердых сторонников. Файловые системы fat, msdos, umsdos и vf at все разделяют общий исходный код. В общем, можно использовать vf at для монтирования разделов Windows FAT-32 (или другой FAT-xx), a umsdos, если нужно использовать раздел FAT в качестве корневой файловой системы для GNU/Linux. Файловые системы Coherent, MINIX, первоначальной System V и Xenix все имеют сходные структуры на диске. Тип файловой системы sysv поддерживает все из них; четыре имени coherent, minix, sysv и xenix являются псевдонимами один для другого. Имена coherent и xenix в конечном счете будут удалены. Быстрая файловая система BSD в течение нескольких лет успешно развилась. Файловая система uf s поддерживает операции чтения/записи для версий, начиная с 4.4 BSD, которая является основой для трех широко распространенных операционных систем BSD: FreeBSD, NetBSD и OpenBSD. Она поддерживает также операции чтения/записи для файловой системы Sun Solaris как для SPARC, так и для систем Intel x86. Первоначальный формат BSD и формат операционной системы NeXTStep поддерживаются в режиме только для чтения. Обозначения «R0» для bef s и ntf s означают, что файловые системы этих типов можно смонтировать и читать, но в них невозможно записать файлы или удалить из них файлы. (Со временем это может измениться; проверьте до.кументацию своей системы.)
232 Глава 8. Файловые системы и обходы каталогов Файловые системы cramf s, iso9660, romf s и udf отмечены «RO», поскольку лежащее в их основе средство по своей сути является устройством только для чтения. Две файловые системы, которых больше не существует, это ext, которая была оригинальной расширенной файловой системой, и xiaf s, которая расширяла оригинальную файловую систему MINIX для использования длинных имен и больших размеров файлов. xiaf s и ext2 появились примерно в одно время, но ext2 в конечном счете стала доминирующей файловой системой4. 8.1.3. Монтирование файловых систем: mount Команда mount монтирует файловые системы, соединяя их содержимое в системную иерархию файлов в их точках монтирования. Под GNU/Linux это иногда запутано, поскольку приходится иметь дело со всеми известными типами файловых систем и их опциями. Обычно запустить mount может лишь root, хотя в некоторых случаях можно сделать исключения, как в случае, обсуждаемом далее в главе. Файловую систему вы указываете с помощью опции -t: mount [ опции ] устройство точка_монтирования Например (# является приглашением для root): # mount -t iso9660 /dev/cdrom /mnt/cdrom /* Монтировать CD-ROM */ # mount -t vfat /dev/fdO /mnt/floppy /* Монтировать гибкий диск MS-DOS */ # mount -t nfs files.example.com:/ /mnt/files /* Монтировать файловую систему NFS */ Можно использовать 4-t auto', чтобы заставить mount угадать тип файловой системы. Это обычно работает, хотя если вы точно знаете, каков тип файловой системы, опция ' -1' помогает указать тип и избежать вероятности ошибочного определения файловой системы, mount по умолчанию угадывает нужный тип, поэтому '-t auto' не является абсолютно необходимым. Системы GNU/Linux предусматривают специальную разновидность монтирования посредством кольцевого (loopback) устройства. Таким способом образ файловой системы, содержащийся в обычном файле, может быть смонтирован, как если бы это было настоящее дисковое устройство. Эта возможность очень полезна, например, при использовании образов CD-ROM. Она позволяет создать образ и испытать его без необходимости прожига на болванке CD с последующим его монтированием. Следующий пример использует первый образ CD из дистрибутива GNU/Linux RedHat 9: # Is -1 shrike-i386-discl.iso /* Исследование файла образа CD */ -rw-r--r-- 1 arnold devel 668991488 Apr 11 05:13 shrike-i386-discl.iso # mount -t iso9660 -o ro,loop shrike-i386-discl.iso /mnt/cdrom /* Смонтировать его в /mnt/cdrom */ # cd /mnt/cdrom /* Перейти туда */ # Is /* Посмотреть файлы */ 4 Источник: http: //www. if e. ee.ethz .ch/music/soFtware/sag/ subsection2_5_4_3 .html. -Примеч. автора.
8.1. Монтирование и демонтирование файловых систем 233 autorun README.it RELEASE-NOTES-fr.html dosutils README.ja RELEASE-NOTES.html EULA README.ко RELEASE-NOTES-it.html GPL README.pt RELEASE-NOTES-j a.html images README.pt_BR RELEASE-NOTES-ко.html isolinux README.zh_CN RELEASE-NOTES-pt_BR.html README README.zh_TW RELEASE-NOTES-pt.html README-Accessibility RedHat RELEASE-NOTES-zh_CN.html README.de RELEASE-NOTES RELEASE-NOTES-zh_TW.html README.es RELEASE-NOTES-de.html'RPM-GPG-KEY README.fr RELEASE-NOTES-es.html TRANS.TBL. # cd /* Сменить */ # umount /xnnt/cdrom /* Демонтировать */ Возможность монтирования таким способом образа ISO 9660 особенно полезна при тестировании сценариев, создающих образы CD. Вы можете создать образ в обычном файле, смонтировать его и проверить, что он подготовлен правильно. Затем, убедившись, что все в порядке, можно скопировать образ на записываемый CD («прожечь» CD). Возможность кольцевого устройства полезна также для монтирования образов гибких дисков. 8.1.4. Демонтирование файловых систем: umount Команда umount демонтирует файловую систему, удаляя ее содержимое из системной иерархии файлов. Использование следующее: umount файл-или-устройство Демонтируемая файловая система не должна быть занята. Это означает, что нет процессов с открытыми в файловой системе файлами и что ни у одного процесса текущий рабочий каталог не находится в этой файловой системе: $.mount /* Показать, что смонтировано */ /dev/hda2 on / type ext3 (rw) /* / находится на настоящем устройстве */ none on /proc type proc (rw) usbdevfs on /proc/bus/usb type usbdevfs (rw) /dev/hda5 on /d type ext3 (rw) /* To же с /d */ none on /dev/pts type devpts (rw,gid=5,mode=620) none on /dev/shm type tmpfs (rw) none on /proc/sys/fs/binfmt_misc,type binfmt_misc (rw) Переключиться на суперпользователя */ Пароль не отображается */ Сделать /d текущим каталогом */ Попытка демонтировать /d */ Bsn't work; it's still in use Сменить /d */ Еще. одна попытка демонтировать /d */ Молчание золото: unmount работает */ $ su Password # # cd /d umount umount: , # # # cd / umount /d /d: /d device is busy / / / / D / / /
234 Глава 8. Файловые системы и обходы каталогов 8.2. Файлы для администрирования файловой системы Файл /etc/ f stab5 перечисляет файловые системы, которые могут быть смонтированы. Большинство из них монтируются автоматически, когда система загружается. Формат следующий: устройство точка-монтирования тип-фс опции dump-freq fsck-pass (dump-frequ fsck-pass являются административными особенностями, не относящимися к настоящему обсуждению). Например, на нашей системе файл выглядит следующим образом: $ cat /etc/fstab # device mount-point type options freq passno /dev/hda3 / ext3 defaults 11/* Корневая файловая система */ /dev/hda5 /d ext3 defaults 1 2 none /dev/pts devpts gid=5,mode=620 0 0 none /proc proc defaults 0 0 none /dev/shm tmpfs defaults 0 0 # Windows partition: /dev/hdal /win vfat noauto,defaults,user,uid=2076,gid=10 0 0 /dev/hda3 swap swap defaults 0 0 /dev/cdrom /mnt/cdrom iso9660 noauto,owner,ro 0 0/* Монтируемый */ /dev/fdO /mnt/floppy auto noauto,owner 0 0 /* Гибкий диск, то же самое */ Разрешены комментарии, начинающиеся с #. Вскоре, в разделе 8.2.1 «Использование опций монтирования», будет обсуждение различных опций. Тот же самый формат файла используется для /etc/mtab, куда mount записывает информацию о файловых системах, когда они смонтированы; umount удаляет информацию из этого файла, когда файловая система демонтирована: $ cat /etc/mtab /dev/hda2 / ext3 rw 0 0 none /proc proc rw 0 0 usbdevfs /proc/bus/usb usbdevfs rw 0 0 /dev/hda5 /d ext3 rw 0 0 none /dev/pts devpts rw,gid=5,mode=620 0 0 none /dev/shm tmpfs rw 0 0 none /proc/sys/fs/binfmt_misc binfmt_misc rw 0 0 /dev/hdal /win vfat rw,noexec,nosuid,nodev,uid=2076,gid=10,user=arnold 0 0 Ядро делает доступным (почти) те же самые сведения в /proc /mounts, в том же формате: $ cat /proc/mounts rootfs / rootfs rw 0 0 Ha GNU/Linux и большинстве систем. Solaris и некоторые системы на основе System V Release 4 используют /etc /vf stab, возможно, с другим форматом. - Примеч. автора
8.2. Файлы для администрирования файловой системы 235 /dev/root / ext3 rw 0 О /ргос /ргос ргос rw О О usbdevfs /proc/bus/usb usbdevfs rw 0 О /dev/hda5 /d ext3 rw 0 0 none /dev/pts devpts rw 0 0 none /dev/shm tmpfs rw 0 0 none /proc/sys/fs/binfmt_misc binfmt_misc rw 0 0 /dev/hdal /win vfat rw,nosuid,nodev,noexec 0 0 ' Обратите внимание, что в /etc/mtab есть некоторые сведения, которые отсутствуют в /ргос/mounts. (Например, см. строку для точки монтирования /win.) С другой стороны, возможно (используя 'mount - f') помещать в / etc /mtab элементы, которые не являются настоящими (эта практика имеет свое применение, см. mount(S)). Подводя итог, /ргос/mounts всегда описывает, что смонтировано в действительности; однако, /etc/mtab содержит сведения об опциях mount, которых нет в /ргос/mounts. Поэтому, чтобы получить полную картину, вам может понадобиться прочесть оба файла. 8.2.1. Использование опций монтирования Команда mount поддерживает опции, которые контролируют то, какие операции ядро будет позволять или не позволять применять к файловой системе. Их приличное количество. Лишь две из них полезны на самом деле в командной строке: го Монтирует файловую систему только для чтения. Это необходимо для устройств только для чтения, таких, как CD-ROM и DVD. loop Использует кольцевое устройство для интерпретации обычного файла в качестве файловой системы. Пример этого мы показали ранее (см. раздел 8.1.3 «Монтирование файловых систем: mount»). Опции передаются с опцией командной строки -о и могут группироваться, отделяясь . запятыми. Например, вот использованная ранее командная строка: mount -t iso9660 -о го,loop shrike-i386-discl.iso /mnt/cdrom Оставшиеся опции предназначены для использования в /etc/f stab (хотя они могут использоваться также и в командной строке). Следующий список представляет те опции, которые, как мы полагаем, наиболее важны в повседневном использовании. auto, noauto Файловые системы, помеченные auto, должны монтироваться при загрузке системы через 'mount -а' (монтировать все файловые системы). Файловые системы noauto должны монтироваться вручную. Они по-прежнему находятся в /etc/f stab вместе с другими файловыми системами. (См., например, элемент /win для показанного ранее файла /etc/f stab).
236 Глава 8. Файловые системы и обходы каталогов defaults Использует по умолчанию опции rw, suid, dev, exec, auto, nouser и async. (async является продвинутой опцией, повышающей производительность ввода/вывода). dev, nodev . Позволяет (не позволяет) использовать в файловой системе файлы символьных или блочных устройств, exec, noexec Позволяет (не позволяет) запускать в файловой системе двоичные исполняемые файлы. user, nouser Позволяет (не позволяет) монтировать данную файловую систему любому пользователю. Это полезно для CD-ROM; даже если вы находитесь на рабочей станции с одним пользователем, удобно не переключаться на root просто для монтирования CD. Демонтировать файловую систему может лишь пользователь, который смонтировал ее. user предполагает наличие опций noexec, nosuid и nodev. suid, nosuid Поддерживает (не поддерживает) биты setuid и setgid исполняемых файлов в файловой системе. rw Монтирует файловую систему в режиме для чтения/записи. Опции nodev, noexec и nosuid особенно ценны для безопасности на файловых системах гибких дисков и CD-ROM. Рассмотрите студенческую среду, в которой студентам разрешено монтировать собственные гибкие диски или CD. Тривиально смастерить для жесткого диска файловую систему с оболочкой setuid-root или с файлом устройства с разрешением записи для всех, которая позволила бы предприимчивому пользователю изменить права доступа к системным файлам. У каждой файловой системы есть специфичные для нее опции. Важной опцией для ext2 и ext3 является grpid. Мы отложим обсуждение этой опции до раздела 11.5.1 «Группа по умолчанию для новых файлов и каталогов». Подробности для всех поддерживаемых файловых систем можно найти в справочной странице mount(8). В качестве конкретного примера вернемся к строчке для раздела Windows в нашей системе: # device mount-point type options freq passno /dev/hdal /win vfat noauto,defaults,user,uid=2076,gid=10 0 0 Опция noauto предотвращает монтирование раздела Windows при загрузке. Опция defaults та же самая, что rw, suid, dev, exec, async. Опция user позволяет нам монтировать файловую систему, не будучи root. Опции uid= и gid= делает файлы в /win принадлежащими нам, так что нам не нужно иметь права root при работе с этим разделом.
8.2. Файлы для администрирования файловой системы 237 8.2.2. Работа со смонтированными файловыми системами: getmntent() Любой из файлов /etc/fstab, /etc/mtab и /proc/mounts может быть прочитан программно с использованием набора процедур getmntent (): #include. <stdio.h> /* GLIBC */ #include <mntent.h> FILE *setmntent(const char *filename/ const char *type); struct mntent *getmntent (FILE *f i'lep) ; int addmntent(FILE *filep, const struct mntent *mnt); int endmntent(FILE *filep); char *hasmntopt(const struct mntent *mnt, const char *opt); setmntent () открывает файл, содержащий элементы точек монтирования. Аргумент filename является файлом, который должен быть открыт. Аргумент type похож на второй аргумент f open (), указывая доступ для чтения, записи или чтения/записи. (Рассмотрите команду mount, которой приходится добавлять элемент к /etc/mtab для каждой файловой системы, которую она монтирует, и umount, которая должна удалять этот элемент.) Затем возвращаемое значение типа FILE* используется с оставшимися процедурами. getmntent () читает файл, возвращая указатель на static struct mntent, которая заполнена соответствующими значениями. Это статическое хранилище переписывается при каждом вызове. Когда больше не осталось элементов, она возвращает NULL. (Это сходно с процедурами для чтения файлов паролей и групп; см. раздел 6.3 «Имена пользователей и групп».) addmntent () вызывается для добавления сведений в конец открытого файла; она предназначена для использования функцией mount. endmntent () закрывает открытый файл; вызывайте ее после завершения обработки. Не вызывайте просто f close (¦); может потребоваться очистить другие внутренние структуры данных, связанные с переменной FILE*. hasmntopt () является более специализированной функцией. Она просматривает struct mntent, переданную в качестве первого параметра, в поисках опции mount, совпадающей со вторым аргументом. Если опция найдена, она возвращает адрес совпадающей подстроки. В противном случае возвращается NULL. Поля в struct mntent непосредственно соответствуют полям в файле /etc/fstab. Структура выглядит следующим образом: struct mntent { char *mnt_f sname; /* Устройство или сервер для файл, с-мы. */ char *mnt_dir; /* Каталог для монтирования. */ char *mnt_type; /* Тип файловой системы: ufs, nfs и т.д. */ char *mnt_opts; /* Отделяемые запятыми опции для fs. */
238 Глава 8. Файловые системы и обходы каталогов int mnt_freq; ¦/* Частота дампа . (в^ днях) . */ int mnt_passno; /* Номер для xfsck'. */ > <• Обычным принципом работы со смонтированными файловыми системами является создание внешнего цикла, читающего /etc/mtab, обрабатывая по одной struct mntent за раз. Наш первый пример, chO8-mounted, с, делает именно это: 1 /* chO8-mounted.с вывод списка смонтированных файловых 2 систем */ 3 /* ЗАМЕЧАНИЕ: специфично для GNU/Linux! */ 4 . 5 #include <stdio.h> 6 #include <errno.h> 7 #include <mntent.h> /* для getmntent() и др. */ 8 #include <unistd.h> /* для getopt() */ 9 10 void process(const char * filename); 11 void print_mount(const struct mntent *fs); 12 13 char *myname; 14 15 /* main обработка опций */ 16 17 int main(int argc, char **argv) 18 { 19 int c; 20 char *file = "/etc/mtab"; /* файл по умолчанию для чтения */ 21 2 2 myname = argv[0]; . 23 while ((c = getopt(argc, argv, "f:")) != -1) {' 24 .switch (c) { 25 case *f': 2 6 file = optarg; 27 break; ' 28 default: , 29 fprintf(stderr, "usage: %s [-f fstab-file]\n", argv[0]); 30 exit(l); 31 } 32 } 33 34 process(file); 3 5 return 0; 36 } 37
8.2. Файлы для администрирования файловой системы 239 38 /* process прочесть структуры struct mntent из файла*/ 39 40 void process(const char *filename) 41 { 42 FILE *fp; 43 struct mntent *fs; 44 45 fp = setmntent(filename, "r"); /* только для чтения */ 46 if (fp == NULL) { 47 fprintf(stderr, "%s: %s: could not open: %s\n", 48 myname, filename, strerror(errno)); 49 exit(l); 50 } 51 52 while ((fs = getmntent(fp)) != NULL) 53 print_mount(fs); 54 55 endmntent(fp); 56 } 57 58 /* print_mount вывод одного смонтированного элемента */ 59 60 void print_mount(const struct mntent *fs) 61 { 62 printf("%s %s %'s %s %d %d\n", 63 fs->mnt_fsname, 64 .fs->mnt_dir, 65 fs->mnt_type/ 66 fs->mnt_opts, 67 fs->mnt_freq, 68 fs->mnt__passno); 69 } В отличие от большинства программ, которые мы до сих пор видели, эта специфична для Linux. Во многих Unix-системах есть схожие процедуры, но их идентичность не гарантируется. По умолчанию, chO8-mounted читает /etc/mtab, выводя сведения о каждой смонтированной файловой системе. Опция -f позволяет указать другой файл для чтения, такой, как /proc/mounts или даже /etc/f stab. Функция main() обрабатывает командную строку (строки 23-32) и вызывает для указанного файла process (). (Эта программа следует нашему стандартному шаблону.) process (), в свою очередь, открывает файл (строка 45) и проходит в цикле через каждую возвращенную файловую систему (строки 52-53). После завершения она закрывает файл (строка 55).
240 Глава 8. Файловые системы и обходы каталогов Функция print_mount () выводит информацию из struct mnent. Вывод во многом напоминает вывод 'cat /etc/mtab': $ ch08-mounted /* Запуск программы */ /dev/hda2 / ext3 rw 0 О none /proc proc rw 0 0 usbdqvfs /proc/bus/usb usbdevfs rw 0 0 /dev/hda5 /d ext3 rw 0 0 none /dev/pts devpts rw,'gid=5,mode=620 0 0 none /dev/shm tmpfs rw 0 0 none /proc/sys/fs/binfmt_misc binfmt_misc rw 0- 0 /dev/hdal /win vfat rw,noexec,nosuid/nodev/uid=2076;gid=10/user=arnold 0 0 8.3. Получение сведений о файловой системе Вывод сведений о файловой системе, рассмотренный ранее - это хорошо и замечательно, но это не захватывает. Раз мы знаем, что определенная точка монтирования представляет файловую систему, нам нужны сведения о файловой системе. Это даст нам возможность выводить вещи наподобие сведений, полученных с помощью df и 'df -i': $ df /* Показать свободное/используемое пространство */ Filesystem lK-blocks Used Available Use% Mounted on /dev/hda2 6198436 4940316 943248 84% / /dev/hda5 61431520 27618536 30692360 48% /d none 256616 0 256616 0% /dev/shm /dev/hdal 8369532 2784700 5584832 34% /win $ df -i /* Показать свободные/используемые индексы */ Filesystem Inodes IUsed IFree IUse% Mounted on /dev/hda2 788704 233216 555488 30% / /dev/hda5 7,815168 503243 7311925 7% /d none 64154 1 64153 1% /dev/shm /dev/hdal 0 0 0 - /win 8.3.1. Стиль POSIX: statvf s () и f statvf s () На ранних системах Unix была только одна разновидность файловой системы. Для них было достаточно, если df считывала суперблок каждой смонтированной файловой системы, извлекала значимые сведения и красиво форматировала их для отображения. (Суперблок обычно был вторым блоком в файловой системе; первым был загрузочный блок, содержащий загрузочный код.) Однако в современном мире такой подход был бы непригодным. POSIX предоставляет расширение XSI для получения доступа к этой информации. Главная функция называется statvf s (). («vfs» часть происходит от лежащей в основе технологии SunOS, использо-
8.3. Получение сведений о файловой системе 241 ванной позже в System V Release 4, которая называется виртуальной файловой системой.) Имеется две функции: #include <sys/types.h> /* XSI */ #include <sys/statvfs.h> int statvfs(const char *path, struct statvfs *buf); int fstatvfs(int fd, struct statvfs *buf); statvfs () использует для любого файла имя пути; она возвращает сведения о файловой системе, содержащей файл, f statvf s () принимает в качестве первого аргумента дескриптор открытого файла; здесь также возвращается информация о файловой системе, содержащей открытый файл, struct statvfs содержит следующие члены: struct statvfs { Размер блока */ Размер фрагмента («основной размер Общее число блоков */ Общее число свободных блоков */ Число доступных блоков (< f_bfree) */ Общее число индексов */ Общее число свободных индексов */ Число доступных индексов (< f_files) */ ID файловой системы */ Флаги: ST_RDONLY и/или STJTOSUID */ Максимальная длина имени файла */ }; Сведений, которые в ней содержатся, достаточно для написания df: unsigned long int f__bsize Размер блока является предпочтительным размером для осуществления ввода/вывода. Файловая система пытается хранить по крайней мере f_bsize байтов стоящих данных в смежных секторах на диске. (Сектор является наименьшим количеством адресуемых данных на диске. Обычно дисковый сектор равен 512 байтам.) unsigned long int f_frsize Некоторые файловые системы (такие, как BSD Fast Filesystem) проводят различие между блоками и фрагментами блоков. Небольшие файлы, общий размер которых меньше размера блока, находятся в некотором числе фрагментов. Это позволяет избежать пустой потери дискового пространства (за счет допустимой цены большей сложности кода ядра). Размер фрагмента выбирается во время создания файловой системы, fsblkcnt_t f_blocks Общее число блоков (в единицах f_bsize) в файловой системе. fsblkcnt_t f_bfree Общее число свободных блоков в файловой системе. unsigned long int f_ unsigned long int f_ блока») */ fsblkcnt_t f„blocks; fsblkcnt__t f_bfree; fsblkcnt_t f_bavail; fsfilcnt_t f_files; fsfilcnt_t f_ffree; fsfilcnt_t f_favail; unsigned long int f_ unsigned long int f_ unsigned long int f_ _bs i z e ; _frsize; /* /* .fsid; .flag; jiamemax; / / / / Чи / / Чи / / • /
242 Глава 8. Файловые системы и обходы каталогов fsblkcnt_t f___bavail ( Число блоков, которые действительно могут использоваться. Некоторые файловые системы резервируют часть блоков файловой системы для использования суперпользователем при заполнении файловой системы. Современные системы резервируют около 5 процентов, хотя это число может быть изменено администратором. (См. tune2fs(8) на системе GNU/Linux и tunefs(S) на системах Unix.) fsfilcnt_t f__files Общее число индексов («порядковых номеров файлов» на языке POSIX) в файловой системе. Это число обычно инициализируется и делается постоянным при создании файловой системы. fsfilcnt_t f_ffree Общее число свободных узлов. fsfilcnt_t¦f_favail Число индексов, которые действительно могут быть использованы. Некоторая часть индексов резервируются для суперпользователя, точно так же, как для блоков. unsigned long int f_fsid ID файловой системы. POSIX не определяет, что оно представляет, и это под Linux не используется. unsigned long int f_flag Флаги, дающие информацию о файловой системе. POSIX определяет два флага: STRDONLY для файловых систем только для чтения (таких, как CD-ROM) и STNOSUID, который запрещает использование битов setuid и setgid в исполняемых файлах. Системы GNU/Linux предусматривают дополнительные флаги: они перечислены в табл. 8.2. Таблица 8.2. Значения GLIBC для f _f lag Флаг POSIX Значение ST__MANDLOCK Осуществляет принудительное блокирование (см. раздел 14.2). ST_NOATIME He обновлять при каждом доступе время доступа. ST_NODEV Запрещает доступ через файлы устройств. ST_JNTODIRATIME He обновлять поле времени доступа каталогов. ST__NOEXEC Запрещает исполнение двоичных файлов. ST_NOSUID V Файловая система запрещает использование битов setuid и setgid. ST_RDONLY V Файловая система только для чтения. ST_SYNCHRONOUS . Любая запись осуществляется синхронно (см. раздел 4.6.3).
8.3. Получение сведений о файловой системе 243 unsigned long int f_namemax Максимальная длина имени файла. Это относится к каждому отдельному компоненту в имени пути; другими словами, максимальная длина для элемента каталога. Типы fsblkcnt_t и fsfilcnt_t определены в <sys/types .h>. Они обычно являются unsigned long, но на современных системах они могут быть даже 64-разрядными, поскольку диски стали очень большими. Следующая программа, ch08-statvf s .с, показывает, как использовать statvf s (): 1 /* ch08-statvfs.с демонстрация statvfs */ 2 3 /* ЗАМЕЧАНИЕ: специфично для GNU/Linux! */ 4 5 #include <stdio.h> 6 #include <errno.h> 7 #include <mntent.h> /*' для getmntent(), et al. */ 8 #include <unistd.h> /* для getopt() */ • 9 #include <sys/types,h> 10 #include <sys/statvfs.h> 11 12 void process(const char * filename); 13 void do_statvfs(const struct mntent *fs) ; 14 15 int errors = 0; 16 char *myname; 17 18 /* main обработка опций */ 19 20 int main(int argc, char **argv) 21 { 22 int c; 23 char *file = "/etc/mtab"; /* файл для чтения по умолчанию */ 24 ' " . 2 5. myname = argv[0]; . 26 while ((с = getopt(argc, argv, " f:")) != -1) { 27 switch (c) { 28 case 'f': 29 file = optarg; 3 0 break; 31 default: 32 fprintf(stderr, "usage: %s [-f fstab-file]\n", argv[0])/ 33 exit(l); 34 } 35 } 36
244 Глава 8. Файловые системы и обходы каталогов 37 process(file); 3 8 return (errors != 0); ,39 } 40 41 /* process чтение структур struct mntent из файла */ 42 43 void process(const char *filename) 44 { 45 FILE *fp; 46 struct mntent *fs; 47 48 fp =i setmntent (filename, "r"); /* только для чтения */ 49 if (fp == NULL) { 50 fprintf(stderr, "%s: %s: could not open: %s\n", 51 myname, filename, strerror(errno)); 52 exit(l); 53 } 54 55 while ((fs = getmntent(fp)) != NULL) 56 do_statvfs(fs); 57 58 endmntent(fp); 59 } Строки 1—59 в сущности те же самые, как и для chO8-mounted. с. main () обрабатывает командную стоку, a process () просматривает в цикле каждую смонтированную файловую систему. do_statvfs() осуществляет действительную работу, выводя для каждой интересующей файловой системы struct statvf s. 61 /* do_statvfs Использовать statvfs и вывести сведения */ 62 63 void do_statvfs(const struct mntent *fs) 64 { 65 struct statvfs vfs; 66 67 if (fs->mnt_fsname[0] != '/') /* пропустить ненастоящие файловые системы */ 68 return; 69 70 if (statvfs(fs->mnt_dir, & vfs) != 0) { 71 fprintf(stderr, "%s: %s: statvfs failed: %s\n", 72 myname, fs->mnt_dir, strerror(errno)); 73 errors++; 7 4 return; 75 } 76
8.3. Получение сведений о файловой системе 245 77 printf("%s, mounted on %s:\n", fs->mnt_dir, fs->mnt_fsname); 78 printf("\tf_bsize: %ld\n"/ (long) vfs.f_bsize); 79 printf("\tf_frsize: %ld\n", (long) vfs.f_frsize); 80 printf("\tf_blocks: %.lu\n", (unsigned long) vfs.f_blocks); 81 printf("\tf__bfree: %lu\n", (unsigned long) vfs.f_bfree); 82 printf("\tf_bavail: %lu\n", (unsigned long) vfs. f__bavail) ; 8.3 printf ("\tf_files: %lu\n", (unsigned long) vfs.f_files); 84 printf("\tf_ffree: %lu\n", (unsigned long) vfs.f_ffree); 85 printf("\tf__favail: %lu\n", (unsigned long) vfs.f_favail); 86 printf("\tf_fsid: %#lx\n", (unsigned long) vfs.f_fsid); 87 88 printf(H\tf_flag: "); 89 if (vfs.f_flag == 0) 90 printf("(none)\n"); 91 else { 92 if ((vfs.f_flag & ST_RDONLY) != 0) 93 printf("ST_RDONLY "); 94 if ((vfs.f_flag & ST_NOSUID) != 0) 95 printf ( "ST_NOSTJID") ; 96 printf("\n"); 97 } 98 99 printf("\tf__namemax: %#ld\n", (long) vfs.f_namemax); 100 } Строки 67-68 пропускают файловые системы, которые не основываются на реальных дисковых устройствах. Это означает, что файловые системы типа /ргос или /dev/pts игнорируются. (Правда, эта проверка эвристическая, но она работает: в /etc/mtab смонтированные устройства перечислены по полному пути устройства: например, /dev/hdal.) Строка 70 вызывает statvf s () с соответствующей проверкой ошибок, а строки 77-99 выводят сведения. Строки 89-96 имеют дело с флагами: отдельные биты информации, которые присутствуют или не присутствуют. Обсуждение того, как биты флагов используются в коде С, см. во врезке. Вот вывод ch08-statvf s: $ ch08-statvfs /* Запуск программы */ Л mounted on /dev/hda2: /* Результаты для, файловой системы ext2 */ f_bsize: 4096 f_frsize: 4096 f_blocks: 1549609 f_bfree: 316663 f_bavail: 237945 f_files: 788704 f_ffree: ,555482 f_favail: 555482
246 Глава 8. Файловые системы и обходы каталогов f_fsid: О f_flag: (none) f_namemax: 2 5 5 /win, mounted on /dev/hdal: /* Результаты для файл, системы vfat */ f__bsize: 4096 f_frsize: 4096 f_blocks: 2092383 f_bfree: 1391952 f_bavail: 1391952 f_files: 0 f_ffree: 0 f_favail: 0 f_fsid: 0 f_flag: ST_NOSUID f_namemax: 2 6 0 Во время написания этого, для GLIBC 2.3.2 и ранее, GNU df не использует statvf s (). Это потому, что код читает /etc/mtab и вызывает stat (¦) для каждой смонтированной файловой системы, чтобы найти ту, номер устройства которой совпадает с соответствующим аргументом для файла (или дескриптора файла). Для того, чтобы прочесть опции монтирования, коду нужно найти файловую систему, поэтому он может установить биты f_f lag. Проблема в том, что stat () на смонтированной удаленной файловой системе, сервер которой недоступен, может висеть неопределенно долго, вызвав также зависание df. С тех пор эта проблема в GLIBC была исправлена, но df не будет изменяться в течение некоторого времени, так что она сможет продолжать работать на более старых системах. 13АМЕЧАНИЕ. Хотя РOSIX определяет statvfs () и fstatvfs (), не все сис- Хтемы их поддерживают или поддерживают корректно. Многие системы {(включая Linux, как вскоре будет описано), имеют свои собственные системные I вызовы, предоставляющие сходную информацию. GNU df использует библио- Хтечную процедуру для получения сведений о файловой системе; исходный файл Хдля этой прог\едуры наполнен #ifdef для большого числа различных систем. \Со временем ситуация с переносимостью должна улучшиться. Битовые флаги Обычной методикой, применимой во многих случаях, является использование набора значений флагов\ когда флаг установлен (т. е. true), имеет место некоторый факт или применяется некоторое условие. Значения флагов определены либо через именованные константы #define, либо через перечисления. В данной главе API nftw() (описанный далее) также использует флаги. Для поля f_f lag структуры struct statvf s есть только два флага:
8.3. Получение сведений о файловой системе 247 #define ST_RDONLY I /* файловая система только для чтения */ #define ST_NOSUID 2 /* setuid/setgid не разрешены */ Физически каждая именованная константа представляет различные позиции битов в значении f_f lag. Логически каждое значение представляет отдельный бит информации о состоянии; т. е. некоторый факт или условие, которое является или не является истинным для данного конкретного экземпляра struct statvfs. Флаги устанавливаются, проверяются и очищаются с помощью побитовых операторов С. Например, statvfs () устанавливает эти флаги, используя побитовый оператор ИЛИ: int statvfs (const char *path, struct sta'tvfs *vfs) { /* заполнить большую часть *vfs */ vfs->f_flag = 0; /* Убедиться, что начинается с нуля */ if {файловая система только для чтения) vfs->f_flag |= ST_RDONLY; /* Добавить флаг ST_RDONLY */ if (файловая система запрещает setuid) vfs->f_flag |= ST_NOSUID; /* Добавить флаг ST_NOSUID */ /* оставшаяся часть процедуры */ } Побитовый оператор И проверяет, установлен ли флаг, а сочетание побитовых операторов И и дополнения очищает флаг: if ((vfs.f_flag & ST_RDONLY) != 0) /* True, если флаг ST_RDONLY */ vfs.f_flag &= HST_RDONLY|ST_NOSUID); /* Очистить оба флага */ Побитовые операторы отпугивают, если вы не использовали их ранее. Однако, только что показанный код примера представляет обычный стиль С. Тщательно изучите каждую операцию; возможно, нарисуйте себе несколько картин, показывающих работу этих операторов. Однажды разобравшись с ними, вы можете тренировать себя, распознавая эти операторы как высокоуровневые операции для управления значениями флагов вместо их трактовки как низкоуровневых манипуляций с битами. Причина использования флагов кроется в том, что они обеспечивают значительную экономию пространства данных. Одно поле unsigned long дает возможность хранить по меньшей мере 32 отдельных бита информации. GLIBC (на момент написания) определяет И различных флагов для поля f_f laga. Если бы вы использовали для каждого флага отдельно поле char, это потребовало бы использования 11 байтов вместо четырех, используемых unsigned long. Если бы у вас было 32 флага, это были бы 32 байта вместо четырех! а. См. /usr/include/bits/statvf s .h на системе GNU/Linux. -Примеч. автора.
248 Глава 8. Файловые системы и обходы каталогов 8.3.2. Стиль Linux: statf s () и fstatf s () Системные вызовы statf s () и f statf s () специфичны для Linux. Их определения следующие: #include <sys/types.h> /* GLIBC */ #include <sys/vfs.h> int statfs(const char *path, struct statfs *buf); int fstatfs(int fd, struct statfs *buf); Как и в случае с statvf s () и f statvf s (), две версии работают с именем файла или с дескриптором открытого файла соответственно, struct statfs выглядит следующим образом: struct statfs { long f_type; /* тип файловой системы */. long f_bsize; /* оптимальный размер блока */ long f„blocks; /* общее число блоков в файловой системе */ long f_bfree; /* число свободных блоков в ф.с. */ long f_bavail; /* свободные блоки, доступные пользователям */ long f__files; /* общее число индексов в файловой системе */ long f_ffree; /* свободных индексов в ф.с. */ fsid_t f_fsid; /* id файловой системы */ long f_namelen; /* максимальная длина имен файлов */ long f__spare[6]; /* запас для дальнейшего */ }; Поля аналогичны полям в struct statvf s. По крайней мере в GLIBC 2.3.2 функции POSIX statvf s () и f statvf s () являются оболочками вокруг statfs () и f statf s () соответственно, копируя значения из одной разновидности структуры в другую. Преимуществом использования statfs () или f statf s () является то, что они системные вызовы. Ядро возвращает информацию непосредственно. Поскольку нет поля f_f lag с опциями монтирования, нет необходимости просматривать каждую смонтированную файловую систему для нахождения нужной. (Другими словами, для заполнения опций монтирования statf vs () должна проверить каждую смонтированную файловую систему, чтобы найти содержащую файл, имя которого содержится в path или f d. Функция statfs () не нуждается в этом, поскольку она не предоставляет сведений об опциях монтирования.) Есть два неудобства в использовании этих вызовов. Во-первых, они специфичны для Linux. Во-вторых, часть сведений из struct statvf s отсутствует в struct statfs; наиболее значительными из них являются флаги (f_f lag) и число доступных индексов (f_f avail). (Поэтому statvf s () Linux приходится находить опции монтирования из других источников, таких, как /etc/mtab, и она «фабрикует» информацию для тех полей struct statvf s, для которых действительные сведения недоступны.)
8.3. Получение сведений о файловой системе 249 Одно поле struct statf s заслуживает особого замечания. Это поле f__type, указывающее тип файловой системы. Значение является магическим числом файловой системы, извлеченной из суперблока. Справочная страница statfsB) предоставляет список обычно используемых файловых систем и их магические числа, которые мы используем в ch08-statf s .с. (Увы, отдельного файла #include нет.) 1 /* ch08-statfs.с демонстрация statfs Linux.*/ 2 3 /* ЗАМЕЧАНИЕ: специфично для GNU/Linux! */ 4 5 #include <stdio.h> 6 #include <errno.h> 7 #include <mntent.h> /* для getmntentO и др.. */ 8 #include <unistd.h> /* для getopt() */ 9 #include <sys/types.h> 10 #include <sys/vfs.h> 11 12 /* Определения взяты из справочной страницы для statfsB): */ 13 #define AFFS_SUPER_MAGIC OxADFF 14 #define EFS_SUPER_MAGIC 0x00414A53 15 #define EXT_SUPER_MAGIC 0xl37D 16 #define EXT2_OLD_SUPER_MAGIC 0xEF51 17 #define EXT2_SUPER_MAGIC 0xEF53 18 #define HPFS_SUPER_MAGIC 0xF995E849 19 #define ISOFS_SUPER_MAGIC 0x9660 20 #define MINIX_SUPER_MAGIC 0xl37F /* оригинальный minix */ 21 #define MINIX__SUPER_MAGIC2 0xl38F /* 30-симв. minix */ 22 #define MINIX2_SUPER_MAGIC 0x2468 /* minix V2 */ 23 #define MINIX2_SUPER_MAGIC2 0x2478 /* minix V2, имена 30 симв. */ 24 #define MSDOS_SUPER_MAGIC 0x4d44 25 #define NCP_SUPER_MAGIC 0x564c 26 #define NFS_SUPER_MAGIC 0x6969 27 #define PROC_SUPER_MAGIC 0x9fa0 2 8 #define SMB_SUPER_MAGIC 0x517B 29 #define XENIX_SUPER_MAGIC 0x012FF7B4 30 #define SYSV4_SUPER_MAGIC 0x012FF7B5 31 #define SYSV2_SUPER_MAGIC 0x012FF7B6 32 #define COH_SUPER_MAGIC 0x012FF7B7 33 #define UFS_MAGIC 0x00011954 34 #define XFS_SUPER_MAGIC 0x58465342 35 #define _XIAFS_SUPER_MAGIC 0x012FD16D 36 37 void process(const char *?ilename); 38 void do_statfs(const struct mntent *fs);
250 Глава 8. Файловые системы и обходы каталогов type2str преобразование типа fs в строку из statfsB) */ type2str(long type) "AFFS" "СОН" } }, 39 40 int errors = 0; 41 char *myname; 42 /* ...main() без изменений, process() почти идентична,.. */ 85 86 I* 87 88 const char 89 { 90 static struct fsname { 91 long type; 92 const char *name; 93 } tablet] = { 94 { AFFS_SUPER_MAGIC 95 { COH_SUPER_MAGIC, 96 { EXT2_OLD_SUPER_MAGIC, "OLD EXT2" 97 { EXT2_SUPER_MAGIC, "EXT2" }, 98 { HPFS_SUPER_MAGIC, "HPFS" }, 99' { ISOFS_SUPER_MAGIC, "ISOFS" }, 100 { MINIX2_SUPER_MAGIC/ 101 { MINIX2_SUPER_MAGIC2 102 { MINIX_SUPER_MAGIC/ 103 { MINIX_SUPER_MAGIC2, 104 { MSDOS_SUPER_MAGIC 105 { NCP_SUPER_MAGIC, 106 { NFS_SUPER_MAGIC/ 107 { PROC_SUPER_MAGIC/ 108 { SMB_SUPER_MAGIC, 109 { SYSV2_SUPER_MAGIC, 110 { SYSV4_SUPER_MAGIC, 111 { UFS_MAGIC/ "UFS" } 112 { XENIX_SUPER_MAGIC, 113 { _XIAFS_SUPER_MAGIC 114 { 0, NULL }, 115 }; 116 static char unknown[100]; 117 int i; 118 119 for (i = 0; table [i] .type ! = 120 if (table[i].type == type) 121 return table[i].name; 122 "MINIX V2" }, "MINIX V2 30 char" MINIX" }, "MINIX 3 0 char" }, Ь , "MSDOS" "NCP" }, "NFS" }, "PROC" } "SMB" }, , "SYSV2" , "SYSV4" 'XENIX" "XIAFS1 }, }, }, }, }, 0; i++)
8.3. Получение сведений о файловой системе 251 123 sprintf(unknown, "unknown type: %#x"; type); 124 return unknown; 125 } 126 127 /* do_statfs Использовать statfs и вывести сведения */ 128 129 void do_statfs(const struct mntent *fs) 130 { 131 struct statfs vfs; 132 133 if (fs->mnt_fsname[0] != '/') /* пропустить фиктивные файловые системы */ 134 return; 13 5 136 if (statfs(fs->mnt_dir/ & vfs) != 0) { 137 fprintf(stderr, "%s: %s: statfs failed: %s\n", 138 myname, fs->mnt_dir, strerror(errno)); 139 errors++; 140 return; 141 } 142 143 printf("%s, mounted on %s:\n", fs->mnt_dir, fs->mnt_f sname); 144 145 printf("\tf_type: %s\n", type2str(vfs.f_type)); 146 printf("\tf_bsize: %ld\n'\ vfs.fjosize); , vfs.f_blocks) vfs.f_bfree); , vfs.f_bavail) vfs.f_files); vfs.f_ffree); 147 printf("\tf_blocks: '%ld\n" 148 printf(M\tf_bfree: %ld\n'\ 149 printf ( "\tf_bavail: %ld\n" 150 printf("\tf_files: %ld\n" 151 printf("\tf_ffree: %ld\n" 152 printf (*" \tf_namelen: %ld\n"/ vf s . f_namelen) ; 153 } Чтобы сохранить место, мы опустили main (), которая не изменилась с представленной ранее другой программы, мы также опустили process (), которая теперь вызывает do_statfs () вместо do__statvfs (). Строки 13-35 содержат список магических чисел файловых систем из справочной страницы statfs{2). Хотя эти числа можно получить из заголовочных файлов исходного кода ядра, это трудно (мы пробовали), а показанному здесь способу представления следовать легче. Строки 86-125 определяют type2str (), которая преобразует магическое число в выводимую строку. Она осуществляет простой линейный поиск в таблице пар (значение, строка). В (маловероятном) случае, когда магическое число в таблице отсутствует, type2str () создает сообщение «неизвестный тип» и возвращает его (строки 123-124).
252 Глава 8. Файловые системы и обходы каталогов do_statfs() (строки 129-153) выводит сведения из struct statfs. Член f_f sid опущен, поскольку f sid_t является непрозрачным типом. Код прост; строка 145 использует type2str () для вывода типа файловой системы. Как для сходной программы, использующей statvfs(), эта функция игнорирует файловые системы, которые не расположены на локальных устройствах (строки 133-134). Вот вывод на нашей системе: $ ch08-statfs /* Запуск программы */ /, mounted on /dev/hda2: /* Результаты для файловой системы ext2 */ f_type: EXT2 f_bsize: 4096 f_blocks: 1549609 f_bfree: 316664 f_bavail: 237946 f_files: 788704 f_ffree: 555483 f„name1en: 2 55 /win, mounted on /dev/hdal: /* Результаты для файловой с-мы vfat */ f_type: MSDOS f_bsize: 4096 f„blocks: 2092383 f_bfree: 1391952 fjoavail: 1391952 f_files: 0 f_ffree: 0 f_namelen: 2 60 В заключение, использование statvf s () или statfs () в вашем собственном коде зависит от ваших потребностей. Как описано в предыдущем разделе, GNU df не использует statvf s () под GNU/Linux и в общем имеет тенденцию использовать уникальный для каждой Unix-системы системный вызов «получения сведений о файловой системе». Хотя это работает, это не очень привлекательно. С другой стороны, иногда у вас нет выбора: например, проблемы GLIBC, о которых мы упоминали выше. В этом случае нет безупречного решения. 8.4. Перемещение по иерархии файлов Несколько системных вызовов и стандартных библиотечных функций дают возможность изменять текущий каталог и определять полный путь к текущему каталогу. Более сложные функции позволяют осуществлять произвольные действия с каждым объектом файловой системы в иерархии каталогов.
8.4. Перемещение по иерархии файлов 253 8.4.1. Смена каталога: chdir () и f chdir () В разделе 1.2 «Модель процессов Linux/Unix» мы говорили: Текущим каталогом является каталог, относительно которого отсчитываются относительные пути (те, которые не начинаются с /). Это каталог, «в» котором вы находитесь, когда даете оболочке команду 4cd некоторое_место\ У каждого процесса есть текущий рабочий каталог. Каждый новый процесс наследует свой текущий каталог от процесса, который его запустил (своего родителя). Две функции позволяют перейти в другой каталог: #include <unistd.h> int chdir(const char *path); /* POSIX */ int fchdirdnt fd) ; /* XSI */ Функция chdir () принимает строку с названием каталога, тогда как f chdir () ожидает дескриптор файла, который был открыт для каталога с помощью open (N. Обе возвращают 0 при успехе и -1 при ошибке (с errno, установленной соответствующим образом). Обычно, если open () для каталога завершается успешно, f chdir () также достигает цели, если кто-то не изменил права доступа к каталогу между вызовами, (f chdir () сравнительно новая функция; на старых системах Unix ее нет.) Использование этих функций почти тривиально. Следующая программа, ch08-chdir. с, демонстрирует обе функции. Она демонстрирует также, что fchdirO может потерпеть неудачу, если права доступа открытого каталога не включают доступа на поиск (исполнение): 1 /* ch08-chdir.с демонстрация chdir() и fchdirO. 2 Для краткости проверка ошибок опущена */ 3 4 #include <stdio.h> 5 #include <fcntl.h> 6 #include <unistd.h> 7 #include <sys/types.h> 8 #include <sys/stat.h> 9 10 int main(void) 11 { 12 int fd; 13 struct stat sbuf; 14 ¦ * 15 fd = open(".", 0_RDONLY); /* открыть каталог для чтения */ 16 fstat(fd, & sbuf); /* получить сведения, нужны начальные права доступа */ 17 chdir("..");/* чcd ..' */ 18 fchmod(fd, 0); /* отменить права доступа каталога */ 19 20 if (fchdir (fd) < 0) /* попытаться выполнить "cd' обратно, должно .завершиться неудачей */ На системах GNU/Linux и BSD для получения нижележащего дескриптора файла можно применить функцию dirf d () к указателю DIR*; см. справочную страницу GNU/Linux diiftiC). - Примеч. автора.
254 Глава 8. Файловые системы и обходы каталогов 21 perror("fchdir back"); 22 2 3 fchmod(fd, sbuf.st_irode & 07777);'/* восстановить первоначальные права доступа */ 24 close(fd); /* все сделано */ 25 2 6 return 0; 27 } Строка 15 открывает текущий каталог. Строка 16 вызывает f stat () для открытого каталога, так что мы получаем копию его прав доступа. Строка 17 использует chdir () для перемещения на один уровень в иерархии файлов. Строка 18 выполняет грязную работу, отменяя все права доступа первоначального каталога. Строки 20-21 пытаются перейти обратно в первоначальный каталог. Ожидается, что эта попытка будет безуспешной, поскольку текущие права доступа не позволяют это. Строка 23 восстанавливает первоначальные права доступа, 'sbuf . st_mode & 07777' получает младшие 12 битов прав доступа; это обычные 9 битов rwxrwxrwx и биты setuid, setgid и «липкий» бит, которые мы обсудим в главе 11 «Права доступа и ID пользователя и группы». Наконец, строка 24 заканчивает работу, закрывая открытый дескриптор файла. Вот что происходит при запуске программы: $ Is -Id . /* Показать текущие права доступа */ drwxr-xr-x 2 arnold devel 4096 Sep 9 16:42 . $ ch08-chdir /* Запустить программу */ fchdir back: Permission denied /* Ожидаемая неудача */ $ Is -Id . /* Снова посмотреть на права доступа */ drwxr-xr-x 2 arnold devel 4096 Sep 9 16:42 . /* Все восстановлено как раньше */ 8.4.2. Получение текущего каталога: getcwd () Названная должным образом функция getcwd () получает абсолютный путь текущего рабочего каталога: #include <unistd.h> /* POSIX */ char *getcwd(char *buf, size__t size); Функция заносит в buf путь; ожидается, что размер buf равен size байтам. При успешном завершении функция возвращает свой первый аргумент. В противном случае, если требуется более size байтов, она возвращает NULL и устанавливает в errno ERANGE. Смысл в том, что если случится ERANGE, следует попытаться выделить буфер большего размера (с помощью malloc () или realloc ()) и попытаться снова. Если любой из компонентов каталога, ведущих к текущему каталогу, не допускает чтения или поиска, getcwd () может завершиться неудачей, a errno будет установлен в EACCES. Следующая простая программа демонстрирует ее использование: /* ch08-getcwd.с демонстрация getcwdO. Проверка ошибок для краткости опущена */ #include <stdio.h>
8.4. Перемещение по иерархии файлов 255 #include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> int main(void) { char buf[PATH_MAX]; char *cp; cp = getcwd(buf, sizeof(buf)); printf("Current dir: %s\n", buf); printf("Changing to ..\n"); chdir(".."); 7* N cd ..' */ cp = getcwd(buf, sizeof(buf)); printf("Current dir is now: %s\n", buf); return 0; } Эта простая программа выводит текущий каталог, переходит в родительский каталог, затем выводит новый текущий каталог. (Переменная ср здесь на самом деле не нужна, но в реальной программе она была бы использована для проверки ошибок.) При запуске программа создает следующий вывод: $ ch08-getcwd Current dir: /home/arnold/work/prenhall/progex/code/ch08 Changing to .. Current dir is now: /home/arnold/work/prenhall/progex/code Формально, если аргумент buf равен NULL, поведение getcwd() не определено. В данном случае версия GLIBC getcwdO вызовет mallocO за вас, выделяя буфер с размером size. Идя даже дальше, если size равен 0, выделяется «достаточно большой» буфер для вмещения возвращенного имени пути. В любом случае вы должны вызвать для возвращенного указателя free () после завершения работы с буфером. Поведение GLIBC полезно, но не переносимо. Для кода, который должен работать на разных платформах, вы можете написать замещающую функцию, которая предоставляет те же самые возможности, в то же время заставив ее непосредственно вызывать getcwd () на системе с GLIBC. Системы GNU/Linux предоставляют файл /proc/self /cwd. Этот файл является символической ссылкой на текущий каталог: $ cd /trap /* Сменить каталог */ $ Is -1 /proc/self/cwd /* Посмотреть на файл */ lrwxrwxrwx I arnold devel 0 Sep 9 17:29 /proc/self/cwd -> /tmp $ cd¦/* Перейти в домашний каталог */ $ Is -1 /proc/self/cwd /* Снова посмотреть на него */ lrwxrwxrwx 1 arnold devel 0 Sep 9 17:30 /proc/self/cwd -> /home/arnold
256 Глава 8. Файловые системы и обходы каталогов Это удобно на уровне оболочки, но представляет проблему на уровне программирования. В частности, размер файла равен нулю! (Это потому, что это файл в /ргос, который продуцирует ядро; это не настоящий файл, находящийся на диске.) Почему нулевой размер является проблемой? Если вы помните из раздела 5.4.5 «Работа с символическими ссылками», Istat () для символической ссылки возвращает в поле st_size структуры struct stat число символов в имени связанного файла. Это число может затем использоваться для выделения буфера соответствующего размера для использования с readlink(). Здесь это не будет работать, поскольку размер равен нулю. Вам придется использовать (или выделять) буфер, который, как вы полагаете, достаточно большой. Однако, поскольку readlink() не выдает символов больше, чем вы предоставили места, невозможно сказать, достаточен буфер или нет; readlinkO не завершается неудачей, когда недостаточно места. (См. в разделе 5.4.5 «Работа с символическими ссылками» функцию Coreutils xreadlink (), которая решает проблему.) В дополнение к getcwd () GLIBC имеет несколько других непереносимых процедур. Они избавляют вас от хлопот по управлению буферами и обеспечивают совместимость со старыми системами BSD. Подробности см. в getcwdC). 8.4.3. Перемещение по иерархии: nftw () Обычной задачей программирования является обработка целых иерархий каталогов: выполнение действий над каждым файлом и каждым каталогом и подкаталогом в целостном дереве. Рассмотрите, например, команду du, которая выводит сведения об использовании диска, 'chown -R\ которая рекурсивно изменяет владельцев, или программу find, которая находит файлы, подходящие по определенным критериям. На данный момент вы знаете достаточно, чтобы написать собственный код для открывания и чтения каталогов вручную, вызова stat () (или Istat ()) для каждого элемента и рекурсивной обработки подкаталогов. Однако, такой код трудно сделать правильным; можно выйти за пределы дескрипторов файлов, если вы будете оставлять родительские каталоги открытыми при обработке подкаталогов; нужно решить, обрабатывать ли символические ссылки как таковые или как файлы, на которые они указывают; нужно суметь справиться с каталогами, которые недоступны для чтения или поиска и т. д. Также трудно писать один и тот же код снова и снова, когда он требуется в нескольких приложениях. 8.4.3.7, Интерфейс nftw () Чтобы избавиться от проблем, System V предложила функцию f tw () («file tree walk» - обход дерева файлов). ftw() осуществляла всю работу по «прохождению» дерева (иерархии) файлов. Вы предоставляете ей указатель на функцию, и она вызывает эту функцию для каждого объекта файла, с которым сталкивается. Ваша функция должна затем обработать каждый объект файловой системы, как считает нужным.
8.4. Перемещение по иерархии файлов 257 Со временем стало ясно, что интерфейс f tw () не вполне выполнял свою работу; например, первоначально он не поддерживал символические ссылки. По этим причинам к Х/Ореп Portability Guide, который теперь является частью POSIX, была добавлена nftwO («new (новая) f tw ()» [важно]). Вот прототип: #include <ftw.h> /* XSI */ int nftw(const char Mir, /* Отправная точка */ int (*fn)(const char *file, /* Указатель функции на */ const struct stat *sb, /* функцию из четырех аргументов */ int flag, struct FTW *s), int depth, int flags); /* Максимум открытых f ds, флаги */ А вот аргументы: const char *dir Строка с именем отправной точки иерархии для обработки. int (*fn) (const char *file, const struct stat *sb, int flag, struct FTW *s) Указатель на функцию с данными аргументами. Эта функция вызывается для каждого объекта в иерархии. Подробности ниже. int depth Этот аргумент назван неверно. Чтобы избежать выхода за пределы дескрипторов файлов, nf tw() держит открытыми не более, чем depth одновременно открытых каталогов. Это не препятствует обработке nftwO иерархий, которые глубже уровня depth; но меньшие значения depth означают, что nf tw () придется делать больше работы, flags Набор флагов, объединяемых побитовым ИЛИ, которые указывают, как nftw() должна обрабатывать иерархию. Интерфейс nf tw() имеет два отдельных набора флагов. Один набор контролирует саму nftw() (аргумент flags функции nftw()). Другой набор передается предоставленной пользователем функции, которую вызывает nftwO (аргумент flags для ( *f n) ()). Однако, интерфейс запутывает, поскольку имена обоих наборов флагов начинаются с префикса 'FTW_\ Мы постараемся сделать все, чтобы это прояснить по ходу дела. В табл. 8.3 представлены флаги, которые контролируют nf tw (). FTW_CHDIR предоставляет большую эффективность; при обработке глубоких - иерархий файлов ядру не приходится обрабатывать снова и снова полные пути имен при осуществлении stat () или открытии каталога. Экономия времени для больших иерархий может быть вполне ощутимой . POSIX стандартизовал f tw () для поддержки существующего кода, aGNU/Linux и коммерческие системы Unix продолжают ее поддерживать. Однако, поскольку она недостаточно функциональна, мы не будем больше ее обсуждать. Если интересуетесь, см. ftwC). - Примеч. автора. 8 У некоторых старых версий GLIBC были проблемы с FTW_CHDIR. Это не относится к GLIBC 2.3.2 и более поздним, и маловероятно, что вы столкнетесь с проблемами. - Примеч. автора. 9-159
258 Глава 8. Файловые системы и обходы каталогов Таблица 8.3. Управляющие флаги для nf tw () Флаг Значение FTW_CHDIR При установке перед открытием каждого из каталогов сначала осуществляется переход в него. Это действие более эффективно, но вызывающее приложение должно быть готово оказаться в другом каталоге, когда nf tw () завершится. При установке осуществляется «сначала глубокий поиск». Это означает, что все файлы и подкаталоги обрабатываются до того, как будет обработан сам каталог. При установке остается в той же самой смонтированной файловой системе. Это более специализированная опция. При установке не следует по символическим ссылкам. FTW_DEPTH может быть, а может и не быть тем, что вам нужно; для некоторых приложений это безусловно справедливо. Рассмотрите 'chmod -R u-rx . \ Эта команда удаляет права чтения и исполнения для владельца для всех файлов и подкаталогов в текущем каталоге. Если это изменение прав доступа применено к каталогу до того, как оно применено к содержимому каталога, любые последующие попытки обработки содержимого потерпят неудачу! Поэтому команда должна применяться к каталогу после обработки его содержимого . Справочная страница GNU/Linux nftwC) отмечает для FTW_PHYS, что «это то, что вам нужно». Это позволяет вам обрабатывать сами символические ссылки, что обычно бывает нужно. (Рассмотрите du; она должна подсчитывать занимаемое ссылками пространство отдельно от связанных с ними файлов.) 8.4.3.2. Функция обратного вызова nftw () После запуска nf tw () она вызывает функцию, указатель для которой предоставляете вы. (Такие функции называются функциями обратного вызова (callback functions), поскольку они «вызываются обратно» из библиотечного кода.) Функция обратного вызова получает четыре аргумента: const char *file Имя текущего обрабатываемого файла (каталога, символической ссылки и т. д.). const struct stat *sb Указатель на struct stat для файла. int flag Одно из нескольких значений флагов (описанных ниже), указывающих, какой это вид файла или была ли ошибка для объекта. Мы не знаем, почему кому-нибудь может, понадобиться делать такое изменение, но философия «что вы просили, то и получили» применяется и здесь! - Примеч. автора. FTW_DEPTH FTW_MOUNT FTW PHYS
8.4. Перемещение по иерархии файлов 259 struct FTW *s Эта структура предоставляет две отдельные части информации: struct FTW { int base; /* Индекс в файле базовой части имени файла */ int level; /* Глубина этого элемента относительно точки отсчета */ }; Параметр flag имеет одно из перечисленных в табл. 8.4 значений. Таблица 8.4. Значения флагов для функции обратного вызова nf tw () Флаг Значение FTW__F Объект является обычным файлом. FTW_D Объект является каталогом. FTW_DNR Объект является каталогом, который нельзя прочесть. FTW_SL Объект является символической ссылкой. FTW_NS Объект не является символической ссылкой, a stat () потерпела неудачу. FTW_DP Объект является каталогом, элементы которого были уже обработаны. Это может случиться, лишь когда в вызове nf tw () использовался FTW_DEPTH. FTW_SLN Объект является символической ссылкой, указывающей на несуществующий файл. Это может случиться, лишь когда в вызове nf tw () не используется FTW_PHYS. struct FTW *s предоставляет дополнительную информацию, которая может быть полезной. s->base действует в качестве индекса в file; file является полным путем обрабатываемого объекта (относительно точки отсчета), 'file + s->base' указывает на первый символ компонента имени файла. s->level указывает текущую глубину иерархии; считается, что первоначальная точка отсчета находится на уровне 0. Функция обратного вызова должна вернуть 0, если все нормально. Любое ненулевое возвращенное значение заставляет nftw() прекратить свою обработку и вернуть то самое ненулевое значение. Справочная страница отмечает, что функция обратного вызова должна останавливать обработку только путем возвращаемого значения, чтобы у nf tw () был шанс произвести очистку: т. е. освободить динамически выделенную память, закрыть открытые дескрипторы файлов и т. д. Функции обратного вызова не следует использовать longjmp (), если только программа не завершается немедленно, (longjmp () является продвинутой функцией, которую мы опишем в разделе 12.5 «Нелокальные goto».) Рекомендуемой методикой обработки ошибок является установка глобальной переменной, указывающей на наличие проблем, возвращение 0 из функции обратного вызова и обработка ошибок после завершения перемещения nf tw () по иерархии файлов. (GNU du это делает, как мы вскоре увидим.) 9*
260 Глава 8. Файловые системы и обходы каталогов Давайте свяжем все это воедино в примере программы. ch08-nf tw. с обрабатывает каждый файл или каталог, указанный в командной строке, запуская для них nftw(). Функция, обрабатывающая каждый файл, выводит с отступом имя и тип файла, показывая иерархическое положение каждого файла. Для разнообразия мы сначала покажем результаты, а затем покажем и обсудим программу: $ pwd /* Где мы находимся */ /home/arnold/work/prenhall/progex v. $ code/ch08/ch08-nftw code /* Обойти каталог xcode' */ code (directory) /* Каталог верхнего уровня */ ch02 (directory) /* Подкаталоги с отступом на один уровень */ ch02-printenv.c (file) /* Файлы в подкаталоге с отступом на два уровня */ ch03 (directory) ch03-memaddr.с (file) ch04 (directory) ch04-holes.c (file) ch04-cat.c (file) ch04-maxfds.с (file) v7cat.c (file) Вот сама программа: I /* ch08-nftw.c демонстрирует nftw() */ 2 3 #define _XOPEN_SOURCE 1 /* Требуется под GLIBC для nftw() */ 4 #define _XOPEN_SOURCE_EXTENDED 1 /* To же V 5 6 #include <stdio.h> 7 #include <errno.h> 8 #include <getopt.h> 9 #include <ftw.h;> /* получает для нас <sys/types,h> и <sys/stat.h> */ 10 #include <limits.h> /* для РАТН_МАХ '*/ II #include <unistd.h> /* для объявлений getdtablesize(), getcwdO */ 12 13 #define SPARE_FDS 5 /* fds для использования другими функциями, см. текст */ 14 ' ' " 15 extern int process(const char *file, const struct stat *sb, 16 int flag, struct FTW *s); 17 18 /* usage print message and die */ 19 20 void usage(const char *name) 21 { 22 fprintf(stderr, "usage: %s [-c] directory ...\n", name); 23 exit(l);
8.4. Перемещение по иерархии файлов 261 24 } 25 26 /* main вызвать nftw() для каждого аргумента командной строки */ 27 28 int main(int argc, char **argv) 29 { 3 0 int i, c, nfds; 31 int errors = 0; 32 int flags ,= FTW_PHYS; 33 char start [PATH__MAXJ , f inish [PATH__MAX] ; 34 35 while ((c = getopt(argc, argv, "c")) != -1) { 3 6 switch (c) { 37 case 'c': 38 flags |= FTW_CHDIR; 3 9 break; 40 default: 41 usage(argv[0] ) ; 42 break; 43 } 44 } . 45 46 if (optind == argc) 47 usage(argv[0]); 48 49 getcwd(start, sizeof start); 50 51 nfds = getdtablesizeO - SPARE_FDS; /* оставить несколько запасных дескрипторов */ 52 for (i = optind; i < argc; i++) { 53 if (nftw(argv[i], process, nfds, flags) != 0) { 54 fprintf(stderr, "%s: %s: stopped early\n", 5 5 argv[0], argv[i]); 56 errors++; 57 } 58 } ' . 59 60 if ((flags & FTW_CHDIR) != 0) { 61 getcwd(finish, sizeof finish); 62 printf("Starting dir: %s\n", start); 63 printf("Finishing dir: %s\n", finish); 64 } / 65 66 return (errors != 0); 67 }
262 Глава 8. Файловые системы и обходы каталогов Строки 3-11 включают заголовочные файлы. По крайней мере в GLIBC 2.3.2 перед включением любого заголовочного файла необходимы #def ine для _XOPEN_SOURCE и _XOPEN_SOURCE_ENTENDED. Они дают возможность получить объявления и значения флагов, которые nf tw() предоставляет свыше предоставляемых f tw(). Это специфично для GLIBC. Потребность в этом в конечном счете исчезнет, когда GLIBC станет полностью совместимой со стандартом POSIX 2001. Строки 35-44 обрабатывают опции. Опция -с добавляет к флагам nftw() FTW_CHDIR. Это эксперимент с целью увидеть, сможете ли вы оказаться где-то в другом месте от того, где начинали. Кажется, это возможно, если nf tw() завершается неудачей; в противном случае вы заканчиваете там же, где начинали. (POSIX не документирует это явным образом, но целью, похоже, было действительно заканчивать там же, где начинали. Стандарт не говорит, что функция обратного вызова не должна менять текущий каталог.) Строка 49 сохраняет начальный каталог для дальнейшего использования, пользуясь getcwd(). Строка 51 вычисляет число дескрипторов, которые может использовать nftw (). Мы не хотим, чтобы она использовала все доступные дескрипторы файлов, если функция обратного вызова также хочет открывать файлы. В вычислении используется getdtablesize() (см. раздел 4.4.1 «Понятие о дескрипторах файлов») для получения максимально возможного числа и вычета из него SPARE_FDS, который был вычислен ранее в строке 13. Эта процедура служит основанием для больших объяснений. В обычном случае по крайней мере три дескриптора уже используются для стандартного ввода, стандартного вывода и стандартной' ошибки. nftw() нужно некоторое количество дескрипторов файлов для открытия и чтения каталогов; внутри себя opendir () использует open () при открытии каталогов для чтения. Если функции обратного вызова также нужно открывать файлы, мы должны предотвратить израсходование функцией nftw() всех доступных дескрипторов файлов для открывания каталогов. Мы делаем это, вычитая некоторое число из максимально допустимого. Для данного примера мы выбрали пять, но если функции обратного вызова нужно открывать файлы, должно использоваться большее число, (nf tw() знает, как восстановиться при израсходовании дескрипторов файлов; мы не должны беспокоиться о таком случае.) Строки 52-58 являются главным циклом над нашими аргументами; строки 53-57 проверяют ошибки; когда они появляются, код выводит диагностическое сообщение и увеличивает значение переменной errors. Строки 60-64 являются частью эксперимента с FTW_CHDIR, выводящего начальный и конечный каталоги, если было использовано -с. По-настоящему интересной функцией является process (); это функция обратного вызова, которая обрабатывает каждый файл. Она использует базовый шаблон для функции обратного вызова nf tw (), который является оператором switch для значения flag: 69 /* process выводит каждый файл на нужном уровне */ 70
8.4. Перемещение по иерархии файлов 263 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 int process(const char *file, const struct int flag, struct FTW *s) { stat *sb, int retval const char = 0; *name = file + s->base; printf ("-%*s" , s->level 1"); /* сделать отступ */ switch (flag) { case FTW_F: printf("%s (file)\n", name); break; case FTW_D: printf("%s . (directory)\n", name); break; case FTW_DNR: printf("%s (unreadable directory)\n", name); break; case FTW_SL: printf("%s (symbolic link)\n", name); break; case FTVOJS: printf("%s (stat failed): %s\n", name, strerror(errno)); break; case FTW_DP: case FTW_SLN: printf("%s: FTW_DP or FTW_SLN: can't happen!\n", name); retval = 1; break; default: printf("%s: unknown flag %d: can't happen!\n", name, flag) retval = 1; break; } return retval; } Строка 75 использует 'file + s->base' для получения имени из полного пути. Это значение указателя сохраняется в переменной name для повторного использования в функции. Строка 77 делает отступ нужного размера, используя красивый трюк. Используя %*s, printf () получает от первого аргумента ширину поля. Это вычисляется динамически как с level * 4'. Строка, которая должна быть выведена - «», пустая строка. Конечным
264 Глава 8. Файловые системы и обходы каталогов результатом является то, что printf () создает для нас отступ нужного размера без необходимости запуска цикла. Строки 79-104 являются оператором switch. В данном случае он не делает ничего весьма интересного, кроме вывода имени файла и его типа (файл, каталог и т. д.). Хотя эта программа не использует struct stat, должно быть ясно, что вы могли бы сделать в функции обратного вызова все, что хотите. ^ЗАМЕЧАНИЕ. Джим Мейеринг (Jim Meyering), сопроводитель GNU Coreutils, I замечает, что дизайн nftw() несовершенен из-за ее рекурсивной природы. (Она {рекурсивно вызывает себя при обработке подкаталогов.) Если иерархия каталогов I становится действительно глубокой, в диапазоне уровней 20 000-40 000 (!), \nftw() может выйти за пределы размера стека, уничтоэгсив программу. Есть \также и другие проблемы, связанные с дизайном nftw(). Версия GNU Coreutils I после 5.0 исправляет это путем использования набора процедур fts () (см. ftsC)). 8.5. Обход дерева файлов: GNU du GNU версия du в GNU Coreutils использует nftw() для обхода одной или более иерархии файлов, собирая и выводя сведения, касающиеся количества используемого дискового пространства. У нее большое число опций, которые управляют ее поведением по отношению к символическим ссылкам, форматом вывода чисел и т. д. Это делает разбор кода труднее, чем могло бы быть при более простой версии. (Однако, мы не собираемся позволить этому остановить нас.) Вот сводка опций du, которые вскоре будут полезны, когда мы рассмотрим код: $ du --help Usage: du [OPTION]... [FILE]... Дает сводку использования диска для каждого FILE, рекурсивно для каталогов. Обязательные для длинных опций аргументы являются обязательными также и для коротких опций. -a, --all записать число всех файлов, а не только каталогов —apparent-size вывести видимые размеры, а не использование диска; хотя видимый размер обычно меньше, он может быть и больше из-за дыр в файлах, внутренней фрагментации, косвенных блоков и т.п. -В, --block-size=SIZE использовать блоки размером SIZE байтов -b, --bytes эквивалентно х--apparent-size --block-size=l' -с, --total выводит итоговую сумму -D, —dereference-args разыменовывывать FILE, которые являются символическими ссылками
8.5. Обход дерева файлов: GNU du 265 -h. --human-readable -H, -k -1, -L, -s, -s, -x, -X FILE --si --count-links --dereference --separate-dirs --summarize --one-file-system --exclude- from=FILE -exclude=PATTERN -max-depth=N --help --version вывести размеры в удобном для восприятия формате (например, IK 234M 2G) так же, но использовать степени 1000, не 1024 подобно --block-size=lK считать размеры несколько раз при прямых ссылках разыменовать все символические ссылки не включать размер подкаталогов отобразить для каждого аргумента лишь итоги пропускать каталоги на различных файловых системах исключить файлы, подходящие под любой образец в FILE исключить файлы, соответствующие PATTERN вывести итог для каталога (или файла, с --all) только если он находится на N или менее уровней глубже аргумента командной строки; —max-depth=0 то же самое, что и -summarize отобразить экран справки и выйти вывести сведения о версии и выйти SIZE может быть (или может быть целым, за которым может следовать это) одним из следующих: кВ 1000, К 1024, MB 1 000 000, М 1 048 576 и т. д. для G, T, P, E, Z, Y. Чтобы еще больше усложнить дело, du использует частную версию nf tw(), у которой есть несколько расширений. Сначала имеются дополнительные значения флагов для функции обратного вызова: FTW_DCHP Это значение означает, что nf tw () не может выполнять 'chdir ("..")'• FTW_DCH Это значение означает, что nftw() не может использовать chdir () для перехода в сам каталог. FTW_DPRE Частная nftw() вызывает для каталогов функцию обратного вызова дважды. Это значение используется при первой встрече с каталогом. После обработки всех нижележащих объектов каталога используется стандартное значение FTW_DP. Частная nf tw () добавляет также в struct FTW новый член, int skip. Если текущий объект является каталогом и функция обратного вызова устанавливает в поле skip
266 Глава 8. Файловые системы и обходы каталогов ненулевое значение, nftw() не будет больше обрабатывать этот каталог. (Функция обратного вызова должна установить skip таким образом, когда flag равен FTW_DPRE; делать это для FTW_DP слишком поздно.) С этим объяснением за поясом, вот функция process_f ile () из du.c. Номера строк приведены относительно начала функции: 1 /* Эта функция вызывается один раз для каждого объекта файловой 2 системы, с которой сталкивается nftw. nftw осуществляет сначала 3 поиск вглубь. Эта функция знает это и собирает итоги для каталогов 4 на основе изменений в глубине текущего элемента. */ .5 6 static int 7 process_file (const char *file, const struct stat *sb, 8 int file_type, struct FTW *info) 9 { . 10 uintmax_t size; 11 uintmax_t size_to_print; 12 static int first_call = 1; 13 static size_t prev_level; 14 static size_t n__alloc; 15 static uintinax_t *sum_ent; 16 static uintmax_t *sum_subdir; 17 int print = 1; 18 19 /* Всегда определяйте info->skip перед возвратом. */ 20 info->skip = excluded_filename (exclude, file + info->base); /* Для --exclude */ Эта функция делает многое, поскольку ей приходится реализовать все опции du. Строка 17 устанавливает print в true A); по умолчанию выводятся сведения о каждом файле. Дальнейший код устанавливает ее при необходимости в false @). Строка 20 устанавливает inf o->skip на основе опции --exclude. Обратите внимание, что это исключает подкаталоги, если каталог совпадает с шаблоном для --exclude. 22 switch (file_type) 23 { 2 4 case FTW_NS: 25 error @, errno, _("cannot access %s"), quote (file)); 2 6 G_fail = 1; /* Установить глобальную переменную для дальнейшего */ 27 return 0; /* Вернуть 0 для продолжения */ 28 2 9 case FTW_DCHP: 3 0 error @, errno, _("cannot change to parent of directory %s"), 31 quote (file)); 32 G_fail =.1; 3 3 return 0;
8.5. Обход дерева файлов: GNU du 267 34 3 5 case FTW_DCH: 36 /* Нельзя просто вернуться, поскольку, хотя nftw не может войти в 37 каталог, она может использовать stat, постольку у нас есть размер */ 38 error @, errno, __("cannot change to directory %s"), quote (file)); 39 G_fail = 1; 40 break; • ' 41 42 case FTW_DNR: 43 /* Нельзя просто вернуться, поскольку, хотя nftw не может прочесть 44 каталог, она может вызвать stat, постольку у нас есть размер. •*/ 45 error @, errno, „("cannot read directory %s"), quote (file)); 46 G_fail = 1; 47 break; 48 49 default: 50 break; 51 .} 52 53 /* Если это первая (предварительная) встреча с каталогом, 54 сразу вернуться. */ 55 if (file_type == FTW_DPRE) 56 return 0; Строки 22-51 являются стандартным оператором switch. Ошибки, для которых нет информации о размере, устанавливают глобальную переменную G_fail в 1 и возвращают 0, чтобы продолжить обработку (см. строки 24-27 и 29-33). Ошибки, для которых есть размер, также устанавливают G_f ail, но затем прерывают switch для того, чтобы обработать статистику (см. строки 35-40 и 42-47). Строки 55-56 сразу завершают функцию, если это первая встреча с каталогом. 58 /* Если файл исключается или если он уже учитывался 59 через прямую ссылку, не включать его в сумму. */ 60 if (info->skip 61 1| (!opt_count_all 62 ScSc 1 < sb->st_nlink 63 ScSc hash_ins (sb->st_ino, sb->st_dev) ) ) 64 { 65 • /* Заметьте, мы не должны здесь просто возвращаться. 66 Нам все еще нужно обновить prev_level и, возможно, передать 67 некоторые суммы выше по иерархии. */ 68 size = 0; 69 print = 0; 70 } 71 else
268 i Глава 8. Файловые системы и обходы каталогов 72 { 73 size = (apparentjsize 74 • ¦ ? sb->st_size 75 : STJSBLOCKS' (*sb) * ST_NBLOCKSIZE); 76 } Теперь становится интересно. По умолчанию du подсчитывает пространство, занимаемое прямыми ссылками, лишь один раз. Опция --count-links заставляет ее подсчитывать пространство для каждой ссылки; переменная opt_count_all равна true, когда указана --count-links. Для отслеживания ссылок du содержит хэш-таблицу10 уже встречавшихся пар (устройство, индекс). Строки 60-63 проверяют, следует ли не включать файл в подсчет, либо из-за того, что он был исключен (inf o->skip равно true, строка 60), либо потому что не была указана --count-links (строка 61) и у файла несколько ссылок (строка 62) и файл уже находится в хэш-таблице (строка 63). В этом случае размер устанавливается в 0, поэтому он не •входит в конечную сумму, a print также устанавливается в false (строки 68-69). Если ни одно из этих условий не отмечается, размер вычисляется либо в соответствии с размером в struct stat, либо в соответствии с числом блоков диска (строки 73-75). Это решение основывается на переменной apparent_size, которая установлена при использовании опции --apparent-size. 78 if (first_call) 79 { 80 n_alloc = info->level + 10; Allocate arrays 81 sum_ent = XCALLOC (uintmax_t/ n_alloc) ; to hold sums 82 sum_subdir = XCALLOC (uintmax__t, n_alloc) ; 83 } 84 else 85 { 86 /* FIXME: Стыдно,.что нам нужно приводить к типу size_t для избежания 87 предупреждений дсс о 'сравнении между знаковым и беззнаковым7. 88 Возможно, неизбежно, при условии, что члены структуры FTW 89 имеют тип хint' (исторически), так как мне нужно, чтобы переменные 90 вроде n_alloc и prev_level имели осмысленные типы. */ 91 if (n_alloc <= (size_t) info->level) 92 { 93 n_alloc = info->level * 2; /* Удвоить сумму */ 94 sum_ent = XREALLOC (sum_ent, uintmax_t, n_alloc); /* И выделить повторно */ 95 sum_subdir = XREALLOC (sum_subdir, uintmax_t, n_alloc) ; 96 } 97 } 98 99 size_to_print = size; Хэш-таблица является структурой данных, позволяющей быстрое получение сохраненной информации; подробности выходят за рамки данной книги. - Примеч. автора.
8.5. Обход дерева файлов: GNU du 269 Строки 78-97 управляют динамической памятью, используемой для хранения статистики о размере файла, f irst_call является статической переменной (строка 12), которая равна true при первом вызове process_f ile(). В этом случае вызывается calldc () (через упаковывающий макрос в строках 81-82; это обсуждалось в разделе 3.2.1.8 «Пример: чтение строк произвольной длины»). Остальную часть времени f irst_call равно false, и используется realloc () (снова через упаковывающий макрос, строки 91-96). Строка 99 заносит значение size в size_to__print; эта переменная может обновляться в зависимости от того, должна ли она включать размеры дочерних элементов. Хотя size могла бы использоваться повторно, отдельная переменная упрощает чтение кода. 101 if (! first_call) 102 { 103 if ((size_t) info~>level == prev_level) 104 { 105 /* Обычно самый частый случай. Ничего не делать. */ 106 } 107 else if ((size_t) info->level > prev_level) 108 { 109 /* Нисхождение по иерархии. 110 Очистить аккумуляторы для *всех* уровней между prev_level 111 и текущим. Глубина может значительно меняться, 112 например, от 1 до 10. */ 113 int i; 114 for (i = prev_level + 1; i <= info->level; i++) 115 sum_ent[i] = sum_subdir[i] = 0; 116 } 117 else /* info->level < prev_level */ 118 { 119 /* Восхождение по иерархии. 120 nftw обрабатывает каталог лишь после всех элементов, 121 в которых был обработан каталог. Когда глубина уменьшается, 122 передать суммы от детей (prev_level) родителям. 123 Здесь текущий уровень'всегда меньше, чем 124 предыдущий. */ 125 . assert ((size_t) info->level == prev_level - 1); 126 size_to_print += sum_ent[prev_level]; 127 if (!opt_separate^dirs) 128 size_to__print += sum_subdir[prev_level]; 129 sum_subdir [info->level] += (sum_ent [prev__level J 130 + sum_subdir[prev_level]); 131 } 132 } Строки 101-132 сравнивают текущий уровень с предыдущим. Возможны три случая.
270 Глава 8. Файловые системы и обходы каталогов Уровни те лее самые В этом случае нет необходимости беспокоиться о статистике дочерних элементов. (Строки 103-106.) Текущий уровень выше предыдущего В этом случае мы спустились по иерархии, и статистику нужно восстановить (строки 107- 116). Термин «аккумулятор» в комментарии подходящий: каждый элемент аккумулирует общее дисковое ^пространство, использованное на этом уровне. (На заре вычислительной техники регистры центрального процессора часто назывались «аккумуляторами»,) Текущий уровень ниже предыдущего В этом случае мы завершили обработку всех дочерних элементов каталога и только что вернулись обратно в родительский каталог (строки 117-131). Код обновляет суммы, включая size_to__print. 134 prev__level = info->level; /* Установить статические переменные */ 135 first_call = 0; 136 137 /* Включить элемент каталога в общую сумму для содержащего 138 каталога, если не указана --separate-dirs (-S). */ 139 if ( ! (opt_separate_dirs && IS_FTW_DIR_TYPE (file^type))) 140 sum_ent [info-*>level] += size; 141 142 /* Даже если каталог нельзя прочесть или перейти в него, 143 включить его размер в общую сумму, ... */ 144 tot_size += size; 145 146 /* ...но не выводить для него итог, поскольку без размера(-ов) 147 потенциальных элементов, это может сильно запутывать. */ 148 if (file_type == FTW_DNR | | file__type == FTW__DCH) • 149 return 0; 150 151 /* Если мы не считаем элемент, например, потому что это прямая 152 ссылка на файл, который уже посчитан (и --count-links), не 153 выводить для него строку. */ 154 if ( '.print) 155 return 0; Строки 134-135 устанавливают статические переменные prev_level и f irst_call таким образом, что они содержат правильные значения для последующего вызова process_f ile (), гарантируя, что весь предыдущий код работает правильно. Строки 137-144 выверяют статистику на основе опций и типа файла. Комментарии и код достаточно просты. Строки 146-155 сразу завершают функцию, если сведения не должны выводиться.
8.5. Обход дерева файлов: GNU du 271 .157 /* FIXME: Это выглядит подозрительно годным для упрощения. */ 158 if ( (IS_FTW_DIR_TYPE ( f ile__type) && 159 (info->level <= max_depth || info->level ==0)) 160 || ( (opt_all ScSc info->level <= max_depth) || info->level == 0)) 161 { 162 print_only_size (size_to_print); 163 fputc ('\t', stdout); 164 if (arg_length) 165 { 166 /* Вывести имя файла, но без суффикса каталога ч.' или N/. ' 167 .который мы, возможно, добавили в main. */ 168 /* Вывести все до добавленной нами части. */ 169 fwrite (file, arg_length, 1, stdout); 170 /* Вывести все после добавленного нами. */ 171 fputs (file + arg_length + suffix_length 172 + (file[arg_length + suffix_length] == '/'), stdout); 173 } 174 else 175 { 176 fputs (-file, stdout) ; 177 } 178 fputc ('\n', stdout); 179 fflush (stdout); 180 } 181 182 return 0; 183 } Условие в строках 158-160 сбивает с толку, и комментарий в строке 157 указывает на это. Условие утверждает: «Если Aа) файл является каталогом и (lb) уровень меньше максимального для вывода (переменные --max-depth и max_depth) или нулевой, или Bа) должны быть выведены все файлы и уровень меньше, чем максимальный для вывода, или BЬ) уровень нулевой», тогда выверти файл. (Версия du после 5.0 использует в этом случае несколько менее запутанное условие.) Строки 162-179 осуществляют вывод. Строки 162-163 выводят размер и символ TAB. Строки 164-173 обрабатывают специальный случай. Это объяснено далее в du. с, в строках файла 524-529: 524 /* При разыменовывавши лишь аргументов командной строки мы 525 используем флаг nftw FTW_PHYS, поэтому символическая ссылка 52 6 на каталог, указанная в командной строке, в норме не 527 разыменовывается. Для решения этого мы идем на издержки, 528 сначала добавляя ч/.' (или ч.'), а затем удаляем их каждый раз 529 при выводе имени производного файла или каталога. */
272 Глава 8. Файловые системы и обходы каталогов В этом случае arg_length равен true, поэтому строки 164-173 должны вывести первоначальное имя, а не измененное. В противном случае строки 174-11711 могут вывести имя как есть. Фу! Куча кода. Мы находим, что это верхний уровень спектра сложности, по крайней мере, насколько это может быть просто представлено в книге данного содержания. Однако, он демонстрирует, что код из реальной жизни часто бывает сложным. Лучшим способом справиться с этой сложностью является ясное именование переменных и подробные комментарии, du. с в этом отношении хорош; мы довольно легко смогли извлечь и изучить код без необходимости показывать все 735 строк программы! 8.6. Изменение корневого каталога: chroot () Текущий рабочий каталог, установленный с помощью chdir() (см. раздел 8.4.1 «Изменение каталога: chdir-() и f chdir () »), является атрибутом процесса, таким же, как набор открытых файлов. Он также наследуется новыми процессами. Менее известным является то, что у каждого процесса есть также текущий корневой каталог. Это именно на этот каталог ссылается имя пути /. В большинстве случаев корневые каталоги процесса и системы идентичны. Однако, супер пользователь может изменить корневой каталог с помощью (как вы догадались) системного вызова chroot (): #include <unistd.h> /* Обычный */ int chroot(const char *path); Возвращаемое значение равно 0 при успешном завершении и -1 при ошибке. Как указывает справочная страница GNU/Linux chrootB), изменение корневого каталога не изменяет текущий каталог: программы, которые должны обеспечить нахождение под новым корневым каталогом, должны также вызвать затем chdir (): if (chroot("/new/root") < 0) /* Установить новый корневой каталог */ /* обработать ошибку */ if (chdir("/some/dir") < 0) /* Пути даны не относительно нового корневого каталога */ /* обработать ошибку */ Системный вызов chroot () чаще всего используется для демонов - фоновых программ, которые должны работать в специальном ограниченном окружении. Например, рассмотрите демон Интернета FTP, допускающий анонимный FTP (соединение любого клиента из любого места, без обычных имени пользователя и пароля). Очевидно, такое соединение не должно быть способным видеть все файлы целой системы. Вместо этого демон FTP выполняет chroot () в специальный каталог со структурой, достаточной лишь чтобы позволить ему функционировать. (Например, со своим собственным /bin/Is для перечисления файлов, со своей копией библиотеки С времени исполнения, если она разделяется, и, воз- Очевидно, в оригинале опечатка: должно быть 177. - Примеч. перге.
8.7. Резюме 273 можно, со своей копией /etc/passwd и /etc/group для отображения ограниченного набора имен пользователей и групп.) POSIX не стандартизует этот системный вызов, хотя GNU/Linux и все системы Unix его поддерживают. (Он популярен с V7.) Он специализирован, но при необходимости очень удобен. 8.7. Резюме Файловые системы являются коллекциями блоков индексов, данных, вспомогательных данных и свободных блоков, организованных особым способом. Файловые системы один к одному соответствуют (физическим или логическим) разделам, на которых они создаются. У каждой файловой системы есть свой корневой каталог; по соглашению, у корневого каталога номер индекса всегда равен 2. Команда mount монтирует файловую систему, наращивая логическое иерархическое пространство имен файлов. Команда unount отсоединяет файловую систему. Ядро делает / . и / . . одним и тем же; корневой каталог всего пространства имен является своим собственным родителем. Во всех остальных случаях ядро устанавливает в корневом каталоге смонтированной файловой системы указывающим на родительский каталог точки монтирования. • Современные Unix-системы поддерживают множество типов файловых систем. В частности, повсеместно поддерживается сетевая файловая система (NFS) Sun, также, как ISO 9660 является стандартным форматом для CD-ROM, а разделы FAT MS- DOS поддерживаются на всех Unix-системах, работающих на платформе Intel x86. Насколько мы знаем, Linux поддерживает наибольшее число различных файловых систем - свыше 30! Многие из них специализированные, но многие из оставшихся предназначены для общего использования, включая по крайней мере четыре различные журнадируемые файловые системы. • Файл /etc/f stab перечисляет разделы каждой системы, их точки монтирования и относящиеся к монтированию опции, /etc/mtab перечисляет те файловые системы, которые смонтированы в настоящее время, то же делает /proc/mounts на системах GNU/Linux. Опция loop функции mount особенно полезна под GNU/Linux для монтирования образов файловых систем, содержащихся в обычных файлах, таких, как образы CD-ROM. Другие опции полезны для безопасности и монтирования внешних файловых систем, таких, как файловые системы vf at Windows. • Файлы формата /etc/fstab можно читать с помощью набора процедур getmntent (). Формат GNU/Linux общий с рядом других коммерческих вариантов Unix, особенно Sun Solaris. Функции statvf s () и f statvf s () стандартизованы POSIX для получения сведений о файловой системе, таких, как число свободных и используемых дисковых блоков, число свободных и используемых индексов и т. д. В Linux есть свой собственный системный вызов для получения подобной информации: statf s () и f statf s (). chdir () и fchdir () дают процессу возможность изменить его текущий каталог. getcwd() получает абсолютное имя пути текущего каталога. Эти три функции просты в использовании. • Функция nf tw () централизует задачу «обхода дерева файлов», т. е. посещения каждого объекта файловой системы (файла, устройства, символической ссылки, ката-
274 Глава 8. Файловые системы и обходы каталогов лога) во всей иерархии каталогов. Ее поведением управляют различные флаги. Программист должен предоставить функцию обратного вызова, которая получает имя каждого файла, struct stat для файла, тип файла и сведения об имени файла и уровне в иерархии. Эта функция может делать для каждого файла все что нужно. Версия функции du из Coreutils 5.0 GNU использует для выполнения этой работы расширенную версию nf tw (). Наконец, системный вызов chroot () изменяет текущий корневой каталог процесса. Это специализированная, но важная возможность, которая особенно полезна для определенных программ в стиле демонов. Упражнения 1. Изучите справочную страницу mount{2) под GNU/Linux и на всех различных системах Unix, к которым у вас есть доступ. Как отличаются системные вызовы? 2. Усовершенствуйте программу ch08-statvf s . с, чтобы она принимала опцию, предоставляющую открытый целый дескриптор файла; для получения сведений о файловой системе она должна использовать f statvf s (). 3. Усовершенствуйте ch08-statvf s . с, чтобы она не игнорировала смонтированные файловые системы NFS. Такие файловые системы имеют устройство в форме server.example.com:/big/disk. 4. Измените ch08-statf s .с (ту, которая использует специфичный для Linux вызов statf s ()), чтобы ее вывод был похож на вывод df. 5. Добавьте опцию -i к программе, которую вы написали для предыдущего упражнения, чтобы ее вывод был такой же, как у 'df -i\ 6. Используя opendir (), readdir (), stat () или f stat (), dirf d () и f chdir (), напишите собственную версию getcwd (). Как вы вычислите общий размер, который должен иметь буфер? Как вы будете перемещаться по иерархии каталогов? 7. Усовершенствуйте свою версию getcwdO, чтобы она выделяла буфер для вызывающего, если первый аргумент равен NULL. 8. Можете ли вы использовать nf tw () для написания getcwd () ? Если нет, почему? 9. Используя nf tw (), напишите свою собственную версию chown, которая принимает опцию -R для рекурсивной обработки целых деревьев каталогов. Убедитесь, что без -R, 'chown пользова тель ка талог" не является рекурсивной. Как вы это проверите? 10. Набор процедур BSD f t s () («file tree stream» - «поток дерева файлов») предоставляет другой способ для обработки иерархии каталогов. У него несколько более тяжелый API как в смысле числа функций, так и структур, которые доступны для вызывающих фукнций уровня пользователя. Эти функции доступны как стандартная часть GLIBC. Прочтите справочную страницу/^C). (Для удобства ее можно распечатать.) Перепишите свою частную версию chown для использования f ts (). 11. Посмотрите справочную страницу find{\). Если бы вы пытались написать find с самого начала, какой набор деревьев файлов вы бы предпочли, nftw() или fts()? . Почему?
Часть 2 Процессы, IPC и интернационализация Глава 9. Управление процессами и каналы 276 Глава 10. Сигналы - 333 Глава 11. Права доступа и ID пользователей и групп - - -385 Глава 12. Общие библиотечные интерфейсы - часть 2 - - - 407 Глава 13. Интернационализация и локализация - 460 Глава 14. Расширенные интерфейсы -499
Глава 9 Управление процессами и каналы В этой главе: 9.1. Создание и управление процессами ; .. . .276 9.2. Группы процесса , 302 9.3. Базовая межпроцессная коммуникация: каналы и FIFO .....' 305 9.4. Управление дескрипторами файлов 310 9.5. Пример: двусторонний канал в gawk . .325 9.6. Рекомендуемая литература 329 9.7. Резюме 329 Упражнения 331 хУак мы говорили в главе 1 «Введение», если бы нужно было резюмировать Unix (а следовательно, и Linux) в трех словах, это были бы «файлы и процессы». Теперь, когда мы увидели, как работать с файлами и каталогами, время взглянуть на оставшуюся часть утверждения: процессы. В частности, мы исследуем, как создаются и управляются процессы, как они взаимодействуют с открытыми файлами и как они могут взаимодействовать друг с другом. Последующие главы исследуют сигналы - грубый способ дать возможность одному процессу (или ядру) сообщить другому о том, что произошло некоторое событие - и проверку прав доступа. В данной главе картина начинает усложняться. В частности, для полноты мы должны упомянуть о вещах, которые не будут рассматриваться до конца главы или до конца книги. В таких случаях мы предусмотрели ссылки вперед, но вы должны быть способны без подготовки уловить суть каждого раздела. 9.1. Создание и управление процессами В отличие от многих предшествующих и последующих операционных систем, создание процессов в Unix задумывалось (и было сделано) дешевым. Более того, Unix разделяет идеи «создания нового процесса» и «запуска данной программы в процессе»1. Это было элегантное проектное решение, которое упрощает многие операции.
9.1. Создание и управление процессами 277 9.1.1. Создание процесса: fork () Первым шагом в запуске новой программы является вызов fork (): #include <sys/types.h>- /* POSIX */ #include <unistd.h> pid_t fork(void); Использование fork () просто. Перед вызовом один процесс, который мы называем родительским, является запущенным. Когда fork () возвращается, имеется уже два процесса: родительский и порожденный (child). Вот ключ: оба процесса выполняют одну и ту лее программу. Два процесса могут различить себя, основываясь на возвращенном fork () значении: Отрицательное Если была ошибка, fork () возвращает -1, а новый процесс не создается. Работу продолжает первоначальный процесс. Нулевое В порожденном процессе fork () возвращает 0. Положительное В родительском процессе fork() возвращает положительный идентификационный номер (PID) порожденного процесса. Код шаблона для создания порожденного процесса выглядит следующим образом: pid_t child; if ((child = fork()) < 0) /* обработать ошибку */ else if (child == 0) /* это новый процесс */ else /* это первоначальный родительский процесс */ pid_t является знаковым целым типом для хранения значений PID. Скорее всего, это просто int, но специальный тип делает код более понятным, поэтому он должен использоваться вместо int. На языке Unix, помимо названия системного вызова, слово «fork» является и глаголом, и существительным . Мы можем сказать, что «один процесс ответвляет другой», и что «после разветвления работают два процесса». (Думайте «развилка (fork) на дороге», а не «вилка (fork), нож и ложка».) Fork (англ.) - «п вилка, развилка; v разветвлять, ответвлять» - Примеч. перев.
278 Глава 9. Управление процессами и каналы 9.1.1.1. После fork (): общие и различные атрибуты Порожденный процесс «наследует» идентичные копии большого числа атрибутов от родителя. Многие из этих атрибутов специализированы и здесь неуместны. Поэтому следующий список намеренно неполон. Существенны следующие: Окружение; см. раздел 2.4 «Окружение». Все открытые файлы и открытые каталоги; см. раздел 4.4.1 «Понятие о дескрипторах файлов» и раздел 5.3.1 «Базовое чтение каталогов». Установки umask; см. раздел 4.6 «Создание файлов». Текущий рабочий каталог; см раздел 8.4.1 «Смена каталога: chdir () и f chdir (). •' Корневой каталог; см. раздел 8.6 «Изменение корневого каталога: chroot ()». Текущий приоритет (иначе называемый «значение nice»; вскоре мы это обсудим; см. раздел 9.1.3 «Установка приоритета процесса: nice () »). • Управляющие терминалы. Это устройство терминала (физическая консоль или окно эмулятора терминала), которому разрешено посылать процессу сигналы (такие, как CTRL-Z для прекращения выполняющихся работ). Это обсуждается далее в разделе 9.2.1 «Обзор управления работой». • Маска сигналов процесса и расположение всех текущих сигналов (еще не обсуждалось; см. главу 10 «Сигналы»). • Реальный, эффективный и сохраненный ID пользователя, группы и набора дополнительных групп (еще не обсуждалось; см. главу И «Права доступа и ID пользователя и группы»). Помимо возвращаемого значения fork () два процесса различаются следующим образом: У каждого есть уникальный ID процесса и ID родительского процесса (PID и PPID). Они описаны в разделе 9.1.2 «Идентификация процесса: getpid k) и getppid ()». • PID порожденного процесса не будет равняться ID любой существующей группы процессов (см. раздел 9.2 «Группы процессов»). • Аккумулированное время использования процессора для порожденного процесса и его будущих потомков инициализируется нулем. (Это имеет смысл; в конце концов, это совершенно новый процесс.) Любые сигналы, которые были ожидающими в родительском процессе, в порожденном сбрасываются, также как ожидающие аварийные сигналы и таймеры. (Мы еще не , рассматривали эти темы; см. главу 10 «Сигналы» и раздел 14.3.3 «Интервальные таймеры: setitimer() и getitimer () ».) Блокировки файлов в родительском процессе не дублируются в порожденном (также еще не обсуждалось; см. раздел 14.2 «Блокировка файлов»). 9.1.1.2. Разделение дескрипторов файлов Атрибуты, которые порожденный процесс наследует от родителя, устанавливаются в те же значения, которые были в родительском процессе в момент выполнения fork (). Однако, с этого момента два процесса продолжают идти собственными путями (большей частью) независимо один от другого. Например, если порожденный процесс изменяет
9.1. Создание и управление процессами 279 каталог, каталог родительского процесса не затрагивается. Сходным образом, если порожденный изменяет среду, среда родителя не меняется. Открытые файлы являются важным исключением из этого правила. Дескрипторы открытых файлов являются разделяемыми, и действия одного процесса с разделяемым дескриптором файла затрагивает состояние файла также и для другого процесса. Это лучше всего понять, изучив рис. 9.L Таблицы дескрипторов процессов PID 42 f Ч PID 45 г г г 1 г 2 г г t t "> Таблица файлов: Учет ресурсов [Учет ресурсойУчет ресурсойУчет ресурсов —-1— f Смещение г Смещение Г Смещение Содержание файла: f [ г Смещение тш»~«»*« Рис. 9.1. Разделение дескрипторов файлов Рисунок отображает внутренние структуры данных ядра. Ключевой структурой данных является таблица файлов. Каждый элемент ссылается на открытый файл. Помимо других учетных данных, таблица файлов содержит текущее положение (смещение чтения/записи) в файле. Оно устанавливается либо автоматически каждый раз при чтении или записи файла, либо непосредственно через lseek () (см. раздел 4.5 «Произвольный доступ: перемещения внутри файла»). Дескриптор файла, возвращенный функциями open () или с г eat (), действует как индекс имеющегося в каждом процессе массива указателей на таблицу файлов. Размер этого массива не превышает значение, возвращенное getdtablesize() (см. раздел 4.4.1 «Понятие о дескрипторах файлов»). На рис. 9.1 показаны два процесса, разделяющие стандартный ввод и стандартный вывод; для каждого из процессов указаны одни и те же элементы в таблице файлов. Поэтому, когда процесс 45 (порожденный) осуществляет read (), общее смещение обновляется; следующий раз, когда процесс 42 (родитель) осуществляет read (), он начинает с позиции, в которой закончила чтение read () процесса 45. Это легко можно видеть на уровне оболочки: $ cat data /* Показать содержание демонстрационного файла */ line 1 line 2
280 Глава 9. Управление процессами и каналы line 3 line 4 $ Is -1 testl ; cat testl /* Режим и содержание тестовой программы */• -rwxr-xr-x l arnold devel 93 Oct 20 22:11 testl #! /bin/sh read line ; echo p: $line /* Прочесть строку в родительской оболочке, вывести ее' */ ( read line ; echo с: $line ) /* Прочесть строку в порожденной оболочке, вывести ее */ read line ; echo p: $line /* Прочесть строку в родительской оболочке, вывести ее */ $ testl < data /* Запустить программу */ р: line 1 /* Родитель начинает сначала */ с: line 2 /* Порожденный продолжает оттуда, где остановился родитель */ р: line 3 /* Родитель продолжает оттуда, где остановился порожденный */ Первая исполняемая строка testl читает из стандартного ввода строку, изменяя смещение файла. Следующая строка testl запускает команды, заключенные между скобками, в подоболочке (subshell). Это отдельный процесс оболочки, созданный ~ как вы догадались - с поМощью fork (). Порожденная подоболочка наследует от родителя стандартный ввод, включая текущее смещение. Этот процесс читает строку и обновляет разделяемое смещение в файле. Когда третья строка, снова в родительской оболочке, читает файл, она начинает там, где остановился порожденный. Хотя команда read встроена в оболочку, все работает таким же образом и для внешних команд. В некоторых ранних Unix-системах была команда line, которая читала одну строку ввода (по одному символу за раз!) для использования в сценариях оболочки; если бы смещение файла не было разделяемым, было бы невозможно использовать такую команду в цикле. Разделение дескрипторов файлов и наследование играют центральную роль в перенаправлении ввода/вывода оболочки; системные вызовы и их семантика делают примитивы уровня оболочки простыми для реализации на С, как мы позже увидим в данной главе. 9.1.1.3. Разделение дескрипторов файлов и close () Тот факт, что несколько дескрипторов файлов могут указывать на один и тот же открытый файл, имеет важное следствие: файл не закрывается до тех пор, пока не будут закрыты все дескрипторы файла. Позже в главе мы увидим, что несколько дескрипторов для одного файла могут существовать не только для разных процессов, но даже и внутри одного и того же процесса; это правило особенно важно для работы с каналами (pipes). Если вам нужно узнать, открыты ли два дескриптора для одного и того же файла, можете использовать f stat () (см. раздел 5.4.2 «Получение сведений о файле») для двух
9.1. Создание и управление процессами 281 дескрипторов с двумя различными структурами struct stat. Если соответствующие поля st_dev и st_i.no равны, это один и тот же файл. Позже в главе мы завершим обсуждение манипуляций с дескрипторами файлов и таблицей дескрипторов файлов. 9.1.2. Идентификация процесса: getpid () и getppid () У каждого процесса есть уникальный ID номер процесса (PID). Два системных вызова предоставляют текущий PID и PID родительского процесса: #include <sys/types.h> /* POSIX */ #include <unistd.h> pid_t getpid(void); pid_t getppid(void); Функции так просты, как выглядят: pid_t getpid (void) Возвращает PID текущего процесса. pid_t getppid (void) Возвращает PID родителя. Значения PID уникальны; по определению, не может быть двух запущенных процессов с одним и тем же PID. PID обычно возрастают в значении, так что порожденный процесс имеет обычно больший PID, чем его родитель. Однако, на многих системах значения PID переполняются-, когда достигается значение системного максимума для PID, следующий процесс создается с наименьшим не используемым номером PID. (Ничто в POSIX не, требует такого поведения, и некоторые системы назначают неиспользуемые номера PID случайным образом.) Если родительский процесс завершается, порожденный получает нового родителя, init. В этом случае PID родителя будет 1, что является PID init. Такой порожденный процесс называется висячим (orphan). Следующая программа, ch09-reparent .с, демонстрирует это. Это также первый пример fork () в действии: I /* ch09-reparent.c показывает, что getppid() может менять значения'*/ 2 3 #include <stdio.h> 4 #include <errno.h> 5 #include <sys/types.h> 6 #include <unistd.h> 7 8 /* main осуществляет работу */ • . 9 10 int main(int argc, char **argv) II { 12 pid_t pid, old__ppid, new__ppid; 13 pid_t child, parent; 14
282 Глава 9. Управление процессами ц каналы 15 parent = getpidO; /* перед fork () */ 16 17 if ((child = forkO) < 0) { 18 fprintf(stderr, "%s: fork of child failed: %s\nM, 19 argv[0], strerror(errno)); 20 exit(l); 21 } else if (child == 0) { 22 old_ppid = getppid(); 23 sleepB); /* см. главу 10 */ 24 new_ppid = getppid(); 25 } else { 26 sleepA); 27 exit@); /* родитель завершается после fork() */ 28 } 29 3 0 /* это выполняет только порожденный процесс */ 31 printf("Original parent: %d\n", parent); 32 printf ("Child: %d\n", getpidO); 33 printf("Child's old ppid: %d\n"/ old_ppid)/ 34 printf("Child's new ppid: %d\n"/ new_ppid); 35 36 exit@); 37 } Строка 15 получает PID начального процесса, используя getpidO. Строки 17-20 создают порожденный процесс, проверяя по возвращении ошибки. Строки 21-24 выполняются порожденным процессом: строка 22 получает РРШ. Строка 23 приостанавливает процесс на две секунды (сведения о sleep () см. в разделе 10.8.1 «Аварийные часы: sleep (), alarm () и SIGALRM»), а строка 24 снова получает PPID. Строки 25-27 исполняются в родительском процессе. Строка 26 задерживает родителя на одну секунду, давая порожденному процессу достаточно времени для осуществления первого вызова getppid (). Строка 27 завершает родителя. Строки 31-34 выводят значения. Обратите внимание, что переменная parent, которая была установлена до разветвления, сохраняет свое значение в порожденном процессе. После порождения у двух процессов идентичные, но независимые копии адресного пространства. Вот что происходит при запуске программы: $ ch09-reparent /* Запуск программы */ $ Original parent: 6582 /* Программа завершается: приглашение оболочки и вывод порожденного процесса */ Child: 6583 Child's old ppid: 6582 Child's new ppid: 1
9.1. Создание и управление процессами 283 Помните, что обе программы выполняются параллельно. Графически это изображено на рис. 9.2. Время О ¦ 1 2 3 4 PID 6582 child » fork() ; sleepA); exit @); 6583 получает нового родителя PID 6583 old_ppid » getppid(); sleepB); Приостановка продолжается new__ppid = getppid () ; Вначале лишь один процесс Порождение процесса Родитель приостановлен, порожденный вызывает getppid () Родитель завершается, порожденный приостановлен Смена родителя порожденного процесса, пока он приостановлен Висячий порожденный процесс вызывает getppid () Рис. 9.2. Два параллельно исполняющихся процесса после разветвления ЗАМЕЧАНИЕ. Использование sleep О, чтобы заставить один процесс пережить другой, работает в большинстве случаев. Однако, иногда случаются ошибки, которые трудно воспроизвести и трудно обнаружить. Единственным способом гарантировать правильное поведение является явная синхронизация с помощью wait () или waitpid(), которые описываются далее в главе (см. раздел 9.1.6.J «Использование функций POSIX: wai t () и waitpid()»). 9.1.3. Установка приоритетов процесса: nice () Когда процессы запущены, ядро динамически меняет приоритет каждого процесса. Как и в жизни, элементы с большим приоритетом получают внимание до элементов с меньшим приоритетом. Короче говоря, каждому процессу выделяется небольщая порция времени для исполнения, которая называется квантом времени (time slice). Когда квант истекает, если текущий процесс все еще является процессом с наивысшим приоритетом, ему разрешается продолжать. Linux, как и Unix, обеспечивает вытесняющую многозадачность. Это означает, что ядро может вытеснить процесс (приостановить его), если настало время дать возможность поработать другому процессу. Приоритет длительное время работающих процессов (например, процессов, выполняющих интенсивные вычисления), снижается в конце их кванта времени, поэтому они дают шанс другим процессам получить время процессора. Сходным образом, процессам, длительное время бездействовавшим в ожидании завершения ввода/вывода (таким, как интерактивный текстовый редактор), приоритет повышается, так что они могут ответить на ввод/вывод, когда он происходит. Короче, ядро гарантирует, что все процессы,
284 Глава 9. Управление процессами и каналы усредненные по времени, получают свою «справедливую долю» времени процессора. Повышение и понижение приоритетов является частью этого процесса. Проектирование хорошего планировщика процессов для ядра является искусством; практические подробности выходят за рамки данной книги. Однако, процесс может влиять на назначения приоритетов ядром посредством своего значения относительного приоритета (nice). Значение относительного приоритета является указанием того, насколько «приятным» хочет быть процесс в отношении других процессов. В соответствии с этим большие значения означают во все большей степени терпеливые процессы; т. е. те, которые все более приятны другим, снижая свор приоритет по отношению к ним. Отрицательное значение относительного приоритета, с другой стороны, означает, что процесс желает быть «менее приятным» по отношению к другим. Такой процесс более эгоистичный, требуя себе большего количества времени процессора3. К счастью, в то время как пользователи могут повышать значение относительного приоритета (быть более приятными), лишь root может снижать значение относительного приоритета (быть менее приятным). Значение относительного приоритета является лишь одним фактором в уравнении, используемом ядром для вычисления приоритета; это не значение самого приоритета, которое изменяется с течением времени на основе поведения процесса и состояния других процессов системы. Для изменения значения относительного приоритета используется системный вызов nice (): #include <unistd.h> /* XSI */ int nice(int inc); Значение относительного приоритета по умолчанию равно 0. Разрешен диапазон значений от-20 до 19. Это требует некоторой привычки. Чемчболее отрицательное значение, тем выше приоритет процесса: -20 является наивысшим приоритетом (наименьшая приятность), а 19 - наинизшим приоритетом (наибольшая приятность). Аргумент inc является приращением, на который надо изменить значение приоритета. Для получения текущего значения, не изменяя его, используйте 'nice@)\ Если результат 'текущий_относительный_приоритет + inc' выйдет за пределы от-20 до 19, система принудительно включит его в этот диапазон. Возвращаемое значение является новым значением относительного Приоритета или -1, если возникла ошибка. Поскольку -1 также является действительным значением относительного приоритета, при вызове nice () следует сначала явным образом установить errno в ноль, а затем проверить его насчет имевшихся проблем: int niceval; int inc = /* любое значение */; errno = 0; if ((niceval = nice(inc)) < 0 && errno != 0) { fprintf(stderr, "nice(%d) failed: %s\n", inc, strerror(errno)); Такие процессы часто демонстрируют детское поведение. - Примеч. автора.
9.1. Создание и управление процессами 285 /* другое восстановление */ } Этот пример может завершиться неудачей, если в inc отрицательное значение, а процесс не запущен как root. 9.1.3.1. POSIX против действительности Диапазон значений относительного приоритета от -20 до 19, которые использует Linux, имеет исторические корни; он ведет начало по крайней мере с V7. POSIX выражает состояние менее прямым языком, что дает возможность большей гибкости, сохраняя в то же время историческую совместимость. Это также затрудняет чтение и понимание стандарта, вот почему вы и читаете эту книгу. Итак, вот как описывает это POSIX. Во-первых, значение относительного приоритета процесса, поддерживаемое системой, колеблется от 0 до 'B * NZERO) - 1'. Константа NZERO определена в <limits ,h> и должна равняться по крайней мере 20. Это дает диапазон 0-39. Во-вторых, как мы описывали, сумма текущего значения относительного приоритета и приращение incr загоняются в этот диапазон. В заключение, возвращаемое nice () значение является значением относительного приоритета процесса минус NZERO. При значении NZERO 20 это дает первоначальный диапазон от -20 до 19, который мы описали вначале. Результатом является то, что возвращаемое nice () значение в действительности изменяется от '-NZERO' до 'NZERO-1', и лучше всего писать свой код в терминах этой именованной константы. Однако, на практике трудно найти систему, в которой NZERO не было бы равно 20. 9.1.4. Запуск новой программы: семейство exec () После запуска нового процесса (посредством fork ()) следующим шагом является запуск в процессе другой программы. Имеется несколько функций, которые служат различным целям: #include <unistd.h> /* POSIX-*/ int execve(const char * filename, /* Системный вызов */ char *const argv[], char *const envp[]); int execl(const char *path, const char *arg, ...); /* Оболочки */ int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[3); int execvp(const char *file, char *const argv[]); Мы ссылаемся на эти функции как на «семейство exec ()». Функции с именем exec () нет; вместо этого мы используем это имя для обозначения любой из перечисленных выше функций. Как и в случае с fork (), «exec» используется на языке Unix и в качестве глагола, означающего исполнение (запуск) программы, и в качестве существительного.
286 Глава 9. Управление процессами и каналы 9.1.4.1. Системный вызов execve () Простейшей для объяснения функцией является execve ().. Она является также лежащим в основе системным вызовом. Другие являются функциями-оболочками, как вскоре будет объяснено. int execve(const char *filename/ char *const argv[], x char * const envpU) filename является именем программы для исполнения. Это может быть именем полного или относительного пути. Файл должен иметь формат исполняемого файла, который понимает ядро. Современные системы используют формат исполняемого файла ELF (Extensible Linking Format - открытый формат компоновки). GNU/Linux распознает ELF и несколько других форматов. С помощью execve () можно исполнять интерпретируемые сценарии, если они используют особую первую строку с именем интерпретатора, начинающуюся с"# ! \ (Сценарии, которые не начинаются с '# !', потерпят неудачу.) В разделе 1.1.3 «Исполняемые файлы» Представлен пример использования 4#! \ argv является стандартным списком аргументов С - массив символьных указателей на строки аргументов, включая значение для использования с argv[0] , завершающийся указателем NULL. envp является окружением для использования новым процессом, с таким же форматом, как глобальная переменная environ (см. раздел 2.4 «Переменные окружения»). В новой программе это окружение становится начальным значением environ. Программа не должна возвращаться из вызова exec (). Если она возвращается, возникла проблема. Чаще всего либо не существует затребованная программа, либо она существует, но не является исполняемой (значения для errno ENOENT и EACCESS соответственно). Может быть множество других ошибок; см. справочную страницу execveB). В предположении, что вызов был успешным, текущее содержимое адресного пространства процесса сбрасывается. (Ядро сначала сохраняет в безопасном месте данные argv и envp.) Ядро загружает для новой программы исполняемый код вместе со всеми глобальными и статическими переменными. Затем ядро инициализирует переменные окружения переданными execve () данными, а далее вызывает процедуру main'O новой программы с переданным функции execve () массивом argv. Подсчетывается число аргументов и это значение передается main () в argc. К этому моменту новая программа запущена. Она не знает (и не может определить), какая программа была в процессе до нее. Обратите внимание, что ID процесса не меняется. Многие другие атрибуты при вызове exec сохраняются; вскоре мы рассмотрим это более подробно. exec () для процесса можно сравнить с ролями, которые играют в жизни люди. В различное время в течение дня один человек может быть родителем, супругом, другом, студентом или рабочим, покупателем в магазине и т. д. Это одна и та же личность, исполняющая различные роли. Также и процесс - его PID, открытые файлы, текущий каталог и т. п. - не изменяются, тогда как выполняемая работа - запущенная с помощью exec () программа - может измениться. См. 9.1.4.3 Имена программ и argv [ 0 ]. - Примеч. науч. ред.
9.1. Создание и управление процессами 287 9.1.4.2. Функции-оболочки: execl () и др. Пять дополнительных функций, действующих в качестве оболочек, предоставляют более удобные интерфейсы для execve (). В первой группе все принимают список аргументов, каждый из которых передается в виде явного параметра функции: int execl(const char *path, const char *arg, ...) Первый аргумент, path, является путем к исполняемому файлу. Последующие аргументы, начиная с arg, являются отдельными элементами, которые должны быть помещены в argv. Как и ранее, явным образом должен быть включен argv [ 0 ]. Вы должны в качестве последнего аргумента передать завершающий указатель NULL, чтобы execl () смогла определить, где заканчивается список аргументов. Новая программа наследует любые переменные окружения, которые находятся в переменной environ, int execlp(const char *file/ const char *arg, ...) Эта функция подобна execl О, но она имитирует механизм поиска команд оболочки, разыскивая file в каждом каталоге, указанном в переменной окружения PATH. Если file содержит символ /, этот поиск не осуществляется. Если PATH в окружении не присутствует, execlp () использует путь по умолчанию. В GNU/ Linux по умолчанию используется " : /bin: /usr /bin", но в других системах может быть другое значение. (Обратите внимание, что ведущее двоеточие в PATH означает, что сначала поиск осуществляется в текущем каталоге.) Более того, если файл найден и имеет право доступа на исполнение, но не может быть исполнен из-за того, что неизвестен его формат, execlp () считает, что это сценарий оболочки и запускает оболочку с именем файла в качестве аргумента. int execle(const char *path, const char *arg, . .., char *const envp[]) Эта функция также подобна execl (), но принимает дополнительный аргумент, envp, который становится окружением новой программы. Как и в случае с execl (), вы должны для завершения списка аргументов поместить перед envp указатель NULL. Вторая группа функций-оболочек принимает массив в стиле argv: int execv(const char *path, char *const argv[]) Эта функция подобна execve (), но новая программа наследует любое окружение, которое находится в переменной environ текущей программы. int execvp(const char *file, char *const argv[]) Эта функция подобна execv (), но она осуществляет такой же поиск в PATH, как и функция execlp () .Она также переходит на исполнение сценария оболочки, если найденный файл не может быть исполнен непосредственно. В табл. 9.1 подведены итоги для шести функций exec (). Функций execlp () и execvp () лучше избегать, если вы не знаете, что переменная окружения PATH содержит приемлемый список каталогов.
288 Глава 9. Управление процессами и каналы Таблица 9.1. Сводка семейства функций exec () по алфавиту Функция ' execl() execle() execlp() execv() execve() execvp() Поиск пути "V V Окружение польз о- Назначение вателя Исполняет список аргументов. Исполняет список аргументов с окружением. Исполняет список аргументов с поиском пути. Исполняет с argv. Исполняет с argv и окружением (системный вызов). Исполняет с argv и с поиском пути. 9.1.4.3. Имена программ и argv [0] До сих пор мы все время считали argv[0] именем программы. Мы знаем, что оно может содержать, а может и не содержать символ /, в зависимости от способа вызова программы; если этот символ содержится, это хорошая подсказка к тому, что для вызова программы использовалось имя пути. Однако, как должно быть ясно к этому времени, то, что argv[0] содержит имя файла, является лишь соглашением. Ничто не может воспрепятствовать передаче вами вызываемой программе в качестве argv[0] произвольной строки. Следующая программа, ch09-run. с, демонстрирует передачу произвольной строки: /* ch09-run.c запуск программы с другим именем и любыми аргументами */ #include <stdio.h> #include <errno.h> #include <unistd.h> /* main настроить argv и запустить указанную программу */ 1 2 3 4 5 б" 7 8 9 int main(int argc, char **argv) 10 { 11 char *path; 12 13 14 15 16 if (argc < 3) { fprintf(stderr, exit(l); } "usage: %s path argO [ arg ]\n", argv[0]);
9.1. Создание и управление процессами 289 17 18 path = argv[l]; 19 2 0 execv(path, argv + 2); /* skip argv[0J and argv[l] */ 21 22 fprintf(stderr, "%s: execvO failed: %s\n", argv[0], 2 3 strerror(errno)); 24 exit(l); 25 } • / Первый аргумент является путем к запускаемой программе, а второй аргумент является новым именем для программы (которое большинство утилит игнорируют, кроме сообщений об ошибках); все остальные аргументы передаются вызываемой программе. Строки 13-16 осуществляют проверку ошибок. Строка 18 сохраняет путь в path. Строка 20 осуществляет exec; если программа доходит до строк 22-23, это указывает на ошибку. Вот что происходит при запуске программы: $ ch09-run /bin/grep whoami foo /* Запустить grep */ a line ¦/*' Входная строка не подходит */ a line with foo in it /* Входная строка подходит */ a line with foo in it /* Это выводится */ AD /* EOF */ $ ch09-run nonexistent-program foo bar /* Демонстрация неудачи */ ch09-run: execv() failed: No such file or directory • Следующий пример несколько неестественен: мы заставили ch09-run запустить себя, передав в качестве имени программы 'foo'. Поскольку аргументов для второго запуска недостаточно, она выводит сообщение об использовании и завершается: $ ch09-run ./ch09-run foo usage: foo path argO [ arg ... ] Хотя она и не очень полезна, chO9-run ясно показывает, что argv [ 0 ] не обязательно должен иметь какое-нибудь отношение к файлу, который в действительности запускается. В System III (примерно в 1980-м) команды ср, In и mv представляли один исполняемый файл с тремя ссылками с этими именами в /bin. Программа проверяла argv[0] и решала, что она должна делать. Это сохраняло некоторое количество дискового пространства за счет усложнения исходного кода и форсирования выполнения программой действия по умолчанию при запуске с неизвестным именем. (Некоторые современные коммерческие системы Unix продолжают эту практику!) Без явной формулировки причин GNU Coding Standards рекомендует, чтобы программы не основывали свое поведение на своем имени. Одна причина, которую мы видели, состоит в том, что администраторы часто устанавливают GNU версию утилиты наряду со стандартной версией коммерческих систем Unix, используя префикс g: gmake, gawk и т. д. Если такие программы ожидают лишь стандартные имена, они при запуске с другим именем потерпят неудачу. 10-159
290 Глава 9. Управление процессами и каналы Сегодня также дисковое пространство дешево; если из одного и того же исходного кода можно построить две почти идентичные программы, лучше это сделать, использовав #ifdef, что у вас есть. Например, grep и egrep имеют значительную часть общего кода, но GNU версия строит два отдельных исполняемых файла. 9.1.4.4. Атрибуты, наследуемые exec () Как и в случае с fork (), после вызова программой exec сохраняется ряд атрибутов: Все открытые файлы и открытые каталоги; см. раздел 4.4.1 «Понятие о дескрипторах файлов» и раздел 5.3.1 «Базовое чтение каталогов». (Сюда не входят файлы, помеченные для закрытия при исполнении (close-on-exec), как описано далее в этой главе; см. раздел 9.4.3.1 «Флаг close-onrexec».) Установки umask; см. раздел 4.6 «Создание файлов». Текущий рабочий каталог; см. раздел 8.4.1 «Изменение каталога: chdir () и f chdir () ». • Корневой каталог; см. раздел 8.6 «Изменение корневого каталога: chroot () ». • Текущее значение относительного приоритета. ID процесса и ID родительского процесса. ID группы процесса и контролирующий терминал; см. раздел 9.2.1 «Обзор управления работами». Маску сигналов процесса и любые ожидающие сигналы, а также любые не истекшие аварийные сигналы или таймеры (здесь не обсуждается; см. главу 10 «Сигналы»). • Действительные ID пользователя и ID группы, а также дополнительный набор групп. Эффективные ID пользователя и группы (а следовательно, и сохраненные ID set-user и set-group) могут быть установлены с помощью битов setuid и setgid исполняемого файла. (Ничто из этого пока не обсуждалось; см. главу 11 «Права доступа и ID пользователя и группы».) Блокировки файлов сохраняются (также пока не обсуждалось; см. раздел 14.2 «Блокировка файлов»). • Суммарное использованное время процессора для процесса и его потомков не меняется. После exec размещение сигналов изменяется; дополнительные сведения см. в разделе 10.9 «Сигналы для fork () и exec () ». После exec все открытые файлы и каталоги остаются открытыми и доступными для использования. Вот как программы наследуют стандартные ввод, вывод и ошибку: они на месте, когда программа запускается. В большинстве случаев при исполнении fork и exec для отдельной программы не нужно ничего наследовать, кроме дескрипторов файлов 0, 1 и 2. В этом случае можно вручную закрыть все другие открытые файлы в порожденном процессе после выполнения fork и до выполнения exec. В качестве альтернативы можно пометить дескриптор файла для автоматического закрытия системой при исполнении exec; эта последняя возможность обсуждается далее в главе (см. раздел 9.4.3.1 «Флаг close-on-exec».)
9.1. Создание и управление процессами 291 9.1.5. Завершение процесса Завершение процесса включает два шага: окончание процесса с передачей системе статуса завершения и восстановление информации родительским процессом. 9.1.5.1. Определение статуса завершения процесса Статус завершения (exit status) (известный также под другими именами значения завершения (exit value), кода возврата (return code) и возвращаемого значения (return value)) представляет собой 8-битовое значение, которое родитель может использовать при завершении порожденного процесса (на языке Unix, «когда порожденный кончается (dies)»). По соглашению статус завершения 0 означает, что программа отработала без проблем. Любое ненулевое значение указывает на какую-нибудь разновидность ошибки; программа определяет используемые числа и их значения, если они есть. (Например, grep использует 0 для указания, что образец был встречен по крайней мере один раз, 1 означает, что образец вообще не встретился, а 2 означает, что возникла ошибка.) Этот статус завершения доступен на уровне оболочки (для оболочек в стиле оболочки Борна) через специальную переменную $?. Стандарт С определяет две константы, которые следует использовать для полной переносимости на не-POSIX системы: EXIT_SUCCESS Программа завершилась без проблем. Для обозначения успеха может также использоваться ноль. EXIT_FAILURE В программе была какая-нибудь проблема. На практике использование лишь этих значений довольно ограничивает. Вместо этого следует выбрать небольшой набор кодов возврата, документировать их значения и использовать. (Например, 1 для ошибок опций командной строки и аргументов, 2 для ошибок ввода/вывода, 3 для ошибок данных и т. д.) Для удобочитаемости стоит использовать константы #def ine или значения enum. Слишком большой список ошибок делает их использование обременительным; в большинстве случаев вызывающая программа (или пользователь) интересуется лишь нулевым или ненулевым значением. Когда достаточно двоичного разделения успех/неудача, педантичный программист использует EXIT_SUCCESS и EXIT_FAILURE. Наш собственный стиль более естественный, используя с return и exit () явные константы 0 или 1. Это настолько обычно, что рано заучивается и быстро становится второй натурой. Однако для своих проектов вы сами должны принять решение. ю*
292 Глава 9. Управление процессами и каналы ЗАМЕЧАНИЕ. Для родительского процесса доступны лишь восемь наименее значимых битов значения. Поэтому следует использовать значения в диапазоне 0-255. Как мы вскоре увидим, у чисел 126 и 127 есть традиционные значения (помимо простого «неуспешно»), которых ваши программы должны придерэюиваться. Поскольку имеют значение лишь восемь наименее значимых битов, вы никогда не должны использовать отрицательные статусы завершения. Когда из небольших отрицательных чисел выделяются восемь последних битов, они превращаются в большие положительные значения! (Например, -1 становится 255, а -5 становится 251.) Мы видели книги по программированию на С, в которых это понималось неправильно - не дайте сбить себя с толку. 9.1.5.2. Возвращение из main () Программа может естественно завершиться одним из двух способов: посредством использования одной из описанных далее функций или возвратившись из main(). (Третий, более радикальный способ описан далее в разделе 12.4 «Совершение самоубийства: abort ()».) В последнем случае следует использовать явное возвращаемое значение вместо выпадения в конце функции: /* Правильно '*/ /* Неправильно */ int main(int argc, char **argv) int main(int argc, char **argv) { { /* здесь код */ /* здесь код */ return 0; /* ?? Что возвращает main()? */ } } Стандарт С 1999 г. указывает, что при выпадении в конце, поведение функции main () должно быть таким, как если бы она возвращала 0. (Это верно также для C++; однако, стандарт С 1989 г. намеренно оставляет этот случай неопределенным.) Во всех случаях плохо полагаться на это поведение; однажды вы можете программировать для системы со скудной поддержкой С времени исполнения, или для внедренной системы, или где-то еще, где это будет по-другому. (В общем, выпадение в конце любой функции, не являющейся void - плохая мысль, которая может вести лишь к ошибочному коду.) Возвращенное из main () значение автоматически передается обратно системе, от которой родительский процесс может его впоследствии получить. Мы опишем, как это делается, в разделе 9.1.6.1 «Использование функций POSIX: wait () и waitpid () ». I ЗАМЕЧАНИЕ. На системах GNU/Linux управляемая компилятором команда с99 запускает компилятор с соответствуюгцими опциями, так что возвращаемое значение при выпадении из конца функции равно 0. Простой gcc этого не делает. 9.1.5.3. Функции завершения Другим способом естественного завершения программы является вызов функций завершения. Стандарт С определяет следующие функции:
9.1. Создание и управление процессами 293 #include <stdlib.h> /* ISO С */ void exit(int status); void _Exit(int status); int atexit(void (^function)(void)); Эти функции работают следующим образом: void exit(int status) Эта функция завершает программу, status передается системе для использования родителем. Перед завершением программы exit () вызывает все функции, зарегистрированные с помощью atexit (), сбрасывает на диск и закрывает все открытые потоки <stdio. h> FILE* и удаляет все временные файлы, созданные tmpf ile () (см. раздел 12.3.2 «Создание и открытие временных файлов»). Когда процесс завершается, ядро закрывает любые оставшиеся открытыми файлы (которые были открыты посредством open (), creat () или через наследование дескрипторов), освобождает его адресное пространство и освобождает любые другие ресурсы, которые он мог использовать, exit () никогда не возвращается. void _Exit(int status) Эта функция в сущности идентична функции POSIX_exit (); мы на короткое время отложим ее обсуждение. int atexit(void (*function)(void)) function является указателем на функцию обратного вызова, которая должна вызываться при завершении программы. exit() запускает функцию обратного вызова перёд закрытием файлов и завершением. Идея в том, что приложение может предоставить одну или более функций очистки, которые должны быть запущены перед окончательным завершением работы. Предоставление функции называется ее регистрацией. (Функции обратного вызова для nftw() обсуждались в разделе 8.4.3.2 «Функция обратного вызова nf tw() »; здесь та же идея, хотя atexit () вызывает каждую зарегистрированную функцию лишь однажды.) atexit () возвращает 0 при успехе или -1 при неудаче и соответствующим образом устанавливает errno. Следующая программа не делает полезной работы, но демонстрирует, как работает atexit(): -' ' /* ch09-atexit.с демонстрация atexit(). Проверка ошибок для краткости опущена. */ /* * Функции обратного вызова здесь просто отвечают на вызов. * В настоящем приложении они делали бы больше. */ void callbackl(void) { printf("callbackl called\n"); } void callback2(void) { printf("callback2 called\n"); > void callback3(void) { printf("callback3 called\n"); } /* main регистрация функций и завершение */
294 Глава 9. Управление процессами и каналы int main(int argc, char **argv) { printf("registering callbackl\n"); atexit(callbackl); printf("registering callback2\n"); atexit(callback2); printf("registering callback3\n"); atexit(callback3); printf("exiting now\n"); exit@); } Вот что происходит при запуске: $ ch09-atexit registering callbackl /* Запуск главной программы */ registering callback2 registering callback3 exiting now callback3 called /* Функции обратного вызова запускаются в обратном порядке */ callback2 called callbackl called Как показывает пример, функции, зарегистрированные с помощью atexit (), запускаются в порядке, обратном порядку их регистрации: последние первыми. (Это обозначается также LIFO - last-in-firs-out - вошедший последним выходит первым.) POSIX определяет функцию _exit(). В отличие от exit О, которая вызывает функции обратного вызова и выполняет <stdio. h> очистку, _exit () является «сразу заканчивающейся» функцией: #include <unistd.h> /* POSIX */ void _exit(int status); Системе передается status, как и для exit (), но процесс завершается немедленно. Ядро все еще делает обычную очистку: все открытые файлы закрываются, использованная адресным пространством память освобождается, любые другие ресурсы, использованные процессом, также освобождаются. На практике функция _Exit () ISO С идентична _exit (). Стандарт С говорит, что от реализации функции зависит, вызывает ли _Exit () зарегистрированные atexit () функции и закрывает ли открытые файлы. Для систем GLIBC это не так, и функция ведет себя подобно _exit (). Время использовать _exit() наступает, когда exec в порожденном процессе завершается неудачей. В этом случае вам не нужно использовать обычный exit(), поскольку это сбрасывает на диск данные буферов, хранящиеся в потоках FILE*. Когда позже родительский процесс сбрасывает на диск свои копии буферов, данные буфера оказываются записанными дважды\ это очевидно нехорошо. Например, предположим, что вы хотите запустить команду оболочки и хотите сами выполнить fork и exec. Такой код выглядел бы следующим образом:
9.1. Создание и управление процессами 295 char *she11command ="..."; pid_t child; if ((child = forkO) ==0) { /* порожденный процесс */ execl("/bin/sh", "sh", "-c", she11command, NULL); _exit(errno == ENOENT ? 127 : 126); } '/* родитель продолжает */ Проверка значения errno и завершающего значения следуют соглашениям, используемым оболочкой POSIX. Если запрошенная программа не существует (ENOENT - нет для нее элемента в каталоге), завершающее значение равно 127. В противном случае, файл существует, но exec не могла быть выполнена по какой-то другой причине, поэтому статус завершения равен 126. Хорошая мысль следовать этим соглашениям также и в ваших программах. Вкратце, чтобы хорошо использовать exit () и at exit (), следует делать следующее: Определить небольшой набор значений статуса завершения, которые ваша программа будет использовать для сообщения этой информации вызывающему. Используйте для них в своем коде константы #def ine или enum. Решить, имеет ли смысл наличие функций обратного вызова для использования catexit(). Если имеет, зарегистрировать их в main() в соответствующий момент; например, после анализа опций и инициализации всех структур данных, которые функция обратного вызова должна очищать. Помните, что функции должны вызываться в порядке LIFO (последняя вызывается первой). • Использовать exit () для выхода из программы во всех местах, когда что-то идет не так и когда выход является правильным действием. Используйте коды ошибок, которые определили. Исключением является rnain(), для которой можно использовать при желании return. Наш собственный стиль заключается обычно в использовании exit () при наличии проблем и 'return 0' в конце main (), если все прошло хорошо. • Использовать _exit() или _Exit() в порожденном процессе, если exec () завершается неудачей. 9.1.6. Использование статуса завершения порожденного процесса Когда процесс заканчивается, нормальным ходом событий для ядра является освобождение всех его ресурсов. Ядро сохраняет статус завершения законченного процесса, также, как сведения о ресурсах, которые он использовал в своей работе, a PID продолжает считаться используемым. Такой завершившийся процесс называется зомби. Родительский процесс, будь то первоначальный родитель или init, может получить статус завершения порожденного процесса. Или, посредством использования функций BDS, которые не стандартизованы POSIX, можно получить статус завершения вместе со сведениями об использовании ресурсов. Использование статуса осуществляется ожиданием окончания процесса: это известно также как пожинание (reaping) процесса5. 5 Мы это не придумываем. Терминология, конечно, не совсем правильна, но таким было чувство юмора разработчиков оригинальной Unix. - Примеч. автора.
296 Глава 9. Управление процессами и каналы Между механизмами, которые ожидают завершения потомков, и сигнальными механизмами, которые мы еще не обсуждали, есть значительное взаимодействие. Что из них описать вначале представляет собой нечто вроде проблемы курицы и яйца; мы решили сначала поговорить сначала о механизмах ожидания порожденного процесса, а глава 10 «Сигналы» дает полный рассказ о сигналах. Пока достаточно понять, что сигнал является способом уведомления процесса о том, что произошло некоторое событие. Процессы могут генерировать сигналы, которые посылаются самим себе, или сигналы могут посылаться извне другими процессами или пользователем за терминалом. Например, CTRL-C посылает сигнал «прерывания», a CTRL-Z посылает сигнал управления работой «стоп». По умолчанию, многие сигналы, такие, как сигнал прерывания, заставляют получающий процесс закончиться. Другие, такие, как сигналы управления работами, вызывают изменение его состояния. Механизмы ожидания порожденного процесса могут определить, претерпел ли процесс сигнал завершения, и если да, какой это был сигнал. То же верно и для остановки процесса и, на некоторых системах возобновления процесса. 9.1.6.1. Использование функций POSIX: wait() и waitpid () Первоначальным системным вызовом V7 был wait (). Более новым вызовом POSIX, основанным на возможностях BSD, является waitpid (). Объявления функций следующие: #include <sys/types.h> /'* POSIX */ #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); wait () ждет завершения любого порожденного процесса; сведения о том, как он завершился, возвращаются в * status. (Вскоре мы обсудим, как интерпретировать *status.) Возвращаемое значение является PID завершившегося процесса или -1, если возникла ошибка. Если порожденных процессов нет, wait () возвращает -1 с errno, установленным в ECHILD (отсутствует порожденный процесс). В противном случае, функция ждет завершения первого порожденного процесса или поступления сигнала. Функция waitpid () Дает возможность ждать завершения определенного порожденного процесса. Она предоставляет значительную гибкость и является предпочтительной для использования функцией. Она также возвращает PID закончившегося процесса или -1 при возникновении ошибки. Аргументы следующие: pid_t "pid Значение указывает, завершения какого порожденного процесса ждать как по-настоящему pid, так и по группе процесса. Смысл значения pid следующий:
9.1. Создание и управление процессами 297 pid < -1 Ждать завершения любого порожденного процесса с ID группы процесса, равной абсолютному значению pid. pid = -1 Ждать завершения любого порожденного процесса. Таким способом работает wait (). pid = 0 Ждать завершения любого порожденного процесса с ID группы процесса, равной ID группе родительского процесса. pid > 0 Ждать завершения конкретного процесса с PID, равным pid. int *status То же, что и для wait (). <sys/wait.h> определяет различные макросы, которые интерпретируют значение в * status, которые мы вскоре опишем. int options Этот параметр должен быть равен либо 0, либо побитовым ИЛИ одного или более из следующих флагов: WNOHANG- Если ни один порожденный процесс не завершился, вернуться немедленно. Таким способом можно периодически проверять, не закончился ли какой- нибудь порожденный процесс. (Такая периодическая проверка известна как опрашивание события.) WUNTRACED Вернуть сведения о порожденном процессе, который остановился, но еще не завершился. (Например, для управления работой.) WCONTINUED (XSI.) Вернуть сведения о порожденном процессе, который продолжился, если его статус не сообщался с момента изменения. Это также для управления работой. Этот флаг является расширением XSI и не доступен под GNU/ Linux. С заполненным значением * status работают несколько макросов, определяющие, что случилось. Они имеют тенденцию образовывать пары: один макрос для определения, что что-то случилось, и если этот макрос истинен, еще один макрос позволяет получить подробности. Макросы следующие: WIFEXITED(status) Этот макрос не равен нулю (true), если процесс завершился (в противоположность изменению состояния). WEXITSTATUS(status) Этот макрос дает статус завершения; он равен восьми наименее значимым битам значения, переданного exit () или возвращенного из main (). Этот макрос следует использовать лишь если WIFEXIDED (status) равен true.
298 Глава 9. Управление процессами и каналы WIFSIGNALED(Status) Этот макрос не равен нулю, если процесс подвергся действию завершающего сигнала death-by-signal. WTERMSIG(status) Этот макрос предоставляет номер сигнала, который завершил процесс. Этот макрос следует использовать, лишь когда WIFSIGNALED (status) равен true. WIFSTOPPED(status) Этот макрос не равен нулю, если процесс был остановлен. WSTOPSIG(status) Этот макрос предоставляет номер сигнала, который остановил процесс. (Процесс остановить могут несколько сигналов.) Этот макрос следует использовать лишь когда WIFSTOPPED (status) равен true. Сигналы управления работами обсуждаются в разделе 10.8.2 «Сигналы управления работой». WIFCONTINUED(status) (XSI.) Этот макрос не равен нулю, если процесс был продолжен. Соответствующего макроса WCONTSIG () нет, поскольку лишь один сигнал может вызвать продолжение процесса. Обратите внимание, что этот макрос является расширением XSI и в частности, он /недоступен в GNU/Linux. Следовательно, если вы хотите его использовать, заключите код внутри '#ifdef WIFCONTINUED ... #endif\ WCOREDUMP(status) (Общий.) Этот макрос не равен нулю, если процесс создал снимок. Снимок процесса (core dump) является образом запущенного процесса в памяти, созданном при завершении процесса. Он предназначен для использования впоследствии при отладке. Системы Unix называют файл core, тогда как системы GNU/Linux используют core, pid, где pid является ID завершившегося процесса. Определенные сигналы завершают процесс и автоматически создают снимок процесса. Обратите внимание, что этот макрос не стандартный. Системы GNU/Linux, Solaris и BSD его поддерживают, однако некоторые другие системы Unix нет. Поэтому и здесь, если нужно его использовать, заключите код внутрь '#if def WCOREDUMP . . . #endif \ Большинство программ не интересуются, почему завершился порожденный процесс; им просто нужно, что он завершился, возможно, отметив, было завершение успешным или нет. Программа GNU Coreutils install демонстрирует такое простое использование fork (), execlp () и wait (). Опция -s заставляет install запустить для устанавливаемого двоичного исполняемого файла программу strip, (strip удаляет из исполняемого файла отладочную и прочую информацию. Это может сохранить значительное пространство. На современных системах с многогигабайтными жесткими дисками при установке редко бывает необходимо использовать strip для исполняемых файлов.) Вот функция strip () из install .с: 513 /* Вырезать таблицу имен из файла PATH. 514 Мы могли бы сначала вытащить из файла магическое число
9.1. Создание и управление процессами 299 515 для определения, нужно ли вырезать, но заголовочные файлы и 516 магические числа варьируют от системы к системе так сильно, что 517 сделать его переносимым было бы очень трудно.'Не стоит усилий. */ 518 519 static void 520 strip (const char *path) 521 { 522 int status; 523 pid_t pid = fork (); 524 525 switch (pid) 526 { 527 case -1: . 528 error (EXIT__FAILURE, errno, _("fork system call failed".)); 529 break; 530 case 0: /* Порожденный. */ 531 execlp ("strip", "strip", path, NULL); 532 error (EXIT_FAILURE, errno, _("cannot run strip")); 533 break; 534 default: /* Родитель. */ 535 /* Родительский процесс. */ 536 while (pid != wait (&status)) /* Ждать завершения потомка. */ 537 /* Ничего не делать. */ ; 538 if (status) 539 error (EXIT_FAILURE, 0, _("strip failed")); 540 break; 541 } 542 } Строка 523 вызывает fork (). Затем оператор switch предпринимает нужное действие для возвращения ошибки (строки 527-529), порожденного процесса (строки 530-533) и родительского процесса (строки 534-539). Стиль строк 536-537 типичен; они ожидают завершения нужного порожденного процесса. Возвращаемое значение wait () является РЮ этого потомка. Оно сравнивается cPID порожденного процесса, status проверяется лишь на предмет равенства нулю (строка 538), в случае ненулевого результата потомок завершился неудачно. (Тест, хотя и правильный, грубый, но простой. Более правильным был бы тест наподобие 'if (WIFEXITEDf status) '&& WEXITSTATUS ( status) != 0)'.) Из описаний и кода, представленных до сих пор, может показаться, что родительские программы должны выбрать определенный момент, чтобы ожидать завершения любого порожденного процесса, возможно, с опросом в цикле (как делает install .с), ожидая всех потомков. В разделе 10.8.3 «Родительский надзор: три различные стратегии» мы увидим, что это необязательно. Скорее, сигналы предоставляют ряд механизмов для использования уведомлениями родителей о завершении порожденных процессов.
300 Глава 9. Управление процессами и каналы 9.1.6.2. Использование функций BSD: wait3 (') nwait4() Системные вызовы BSD wait3() и wait4() полезны, если вы интересуетесь ресурсами, использованными порожденным -процессом. Функции нестандартны (что означает, что они не являются частью POSIX), но широко доступны, в том числе на GNU/ Linux. Объявления следующие: #include <sys/types.h> /* Обычный */ #include <sys/time.h> /* Под GNU/Linux не нужно, но улучшает переносимость */ #include <sys/resource.h> #include <sys/wait.h> pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid__t pid, int *status, int options, struct rusage *rusage); Переменная status та же, что и для wait () и waitpid(). Все описанные ранее макросы (WIFEXITED О и т. д.) могут использоваться и с ними. Значение options также то же самое, что и для waitpid (): либо 0, либо побитовое ИЛИ с одним или обоими флагами WNOHANG и WUNTRACED. wai13 () ведет себя подобно wait О, получая сведения о первом доступном порожденном зомби, a wait4 () подобна waitpid (), получая сведения об определенном процессе. Обе функции возвращают PID потомка, -1 при ошибке или 0, если нет доступных процессов и был использован флаг WNOHANG. Аргумент pid может принимать те же значения, что и аргумент pid для waitpid (). Ключевым отличием является указатель struct rusage. Если он не равен NULL, система заполняет ее сведениями о процессе. Эта структура описана в POSIX и в справочной странице getrusageB): struct rusage { struct timeval ru_utime; struct timeval ru_stime; long long long long long long long long long long long long long long ru_maxrss; ru__ixrss; ru_idrss; ru_isrss; ru_jninf It/ ru_majfIt; ru__nswap / ru_inblock; ru__oublock; ru_msgsnd; ru_jnsgrcv; ru_nsignals; ru_nvcsw; ru__nivcsw; 7* /* /* /* /* ' /* /* /* /* /* /* /* /* /* /* используемое время пользователя */ /* используемое системное время */ максимальный размер резидентного набора */ общий размер разделяемой памяти */ общий размер не разделяемых данных */ общий размер не разделяемого стека */ использование страниц */ ошибок страниц */ подкачек */ блочных операций ввода */ блочных операций вывода */ посланных сообщений */ полученных сообщений */ полученных сигналов */ добровольных переключений контекста */ принудительных переключений контекста */
9.1. Создание и управление процессами 301 Чисто BSD системы D.3 Reno и более поздние) поддерживают все поля. В табл. 9.2 описаны доступность различных полей struct rusage для POSIX и Linux. Таблица 9.2. Доступность полей sturct rusage ru_utime ru_stime ru_minfIt ru_majfIt POSIX V V >2.4 ?2.4 >2.4 >2.4 Поле ru_nswap ru_nvcsw ru_nivcsw POSIX Linux > 2.4 >2.6 >2.6 Стандартом определены лишь поля, помеченные «POSIX». Хотя Linux определяет полную структуру, ядро 2.4 поддерживает лишь поля времени пользователя и системного времени. Ядро 2.6 поддерживает также поля, связанные с переключением контекста6. Наиболее интересными полями являются ru__utime и ru_stime, использование времени процессора в режиме пользователя и ядра соответственно. (Время процессора в режиме пользователя является временем, потраченным на исполнение кода уровня пользователя. Время процессора в режиме ядра является временем, потраченным в ядре в пользу процесса.) Эти два поля используют struct timeval, которая содержит значения времени с точностью до микросекунд. Дополнительные сведения по этой структуре см. в разделе 14.3.1 «Время в микросекундах: gettimeof day () ». В BSD 4.2 и 4.3 аргумент status функций wait () и wait3 () был union wait. Он умещался в int и предоставлял доступ к тем же сведениям, которые выдают современные макросы WIFEXITDE () и др., но через членов объединения. Не все члены были действительными во всех случаях. Эти члены и их использование описаны в табл. 9.3. POSIX не стандартизует union wai t, a BSD 4.4 не документирует его, используя вместо этого макросы POSIX. GLIBC делает несколько бросков, чтобы заставить использующий его старый код продолжать работать. Мы опишем его здесь главным образом для того, чтобы вы увидев его - узнали; новый код должен использовать макросы, описанные в разделе 9.1.6.1 «Использование функций POSIX: wait () и waitpid () ». Таблица9.3. union wait 4.2 и 4.3 BSD Макрос POSIX Член объединения Использование Значение WIFEXITEDO w_termsig w.w_termsig ==0 True при нормальном завершении WEXITSTATUS () w_retcode code = Статус завершения, если w.w_retcode не по сигналу 6 Дважды проверьте справочную страницу getrusage{2), если у вас более новое ядро, поскольку это поведение, возможно, изменилось. - Примеч. автора.
302 Глава 9. Управление процессами и каналы Таблица 9.3. union wait 4.2 и 4.3 BSD (Продолжение) Макрос POSIX Член объединения Использование Значение WIFSIGNALEDO WTERMSIGO WIFSTOPPEDO WSTQPSIGO WCOREDUMP() w_termsig w_termsig w_stopval w_stopsig w__coredump w.w_temsig != 0 True, если завершен no сигналу sig = w.w_termsig Сигнал, вызвавший завершение w.w_stopval == WSTOPPED True, если остановлен sig = w.w__stopsig Сигнал, вызвавший остановку w. w_coredunip ! = 0 True, если потомок сделал снимок образа 9.2. Группы процессов Группа процесса является группой связанных процессов, которые в целях управления заданием (job) рассматриваются вместе. Процессы с одним и тем же ГО группы процессов являются членами группы процессов, а процесс, PID которого равен ID группы процессов, является лидером группы процессов. Новые процессы наследуют© группы процессов своих родительских процессов. Мы уже видели, что wai tpid () позволяет вам ждать любой процесс в данной группе процессов. В разделе 10.6.7 «Отправка сигналов: kill () и killpg () » мы увидим также, что вы можете отправить сигнал всем процессам в определенной группе процессов. (Всегда применяется проверка прав доступа; вы не можете послать сигнал процессу, которым не владеете.) 9.2.1. Обзор управления заданиями Управление заданиями является сложной темой, той, в которую мы решили не погружаться в данной книге. Однако, здесь приведен краткий концептуальный обзор. Устройство терминала (физическое или другое) с работающим на нем пользователем называется управляющим терминалом. Сеанс (session) является коллекцией групп процессов, связанных с управляющим терминалом. На одном терминале имеется лишь один сеанс, с несколькими группами процессов в сеансе. Один процесс назначен лидером сеанса; обычно это оболочка, такая, как Bash, pdksh, zsh или ksh93 , которая может осуществлять управление заданиями. Мы называем такую оболочку оболочкой, управляющей заданиями. Каждое задание, запущенное управляющей заданиями оболочкой, будь то простая программа или конвейер, получает отдельный идентификатор группы процессов. Таким способом оболочка может манипулировать заданием как отдельной сущностью, хотя в нем может быть несколько процессов. 7 csh и tcsh также могут быть включены в эту категорию, но мы предпочитаем оболочки в стиле оболочки Борна. - Примеч. автора. *
9.2. Группы процессов 303 Управляющий терминал также имеет связанный с ним идентификатор группы процессов. Когда пользователь набирает специальный символ, такой, как CTRL-C для «прерывания» или CTRL-Z для «остановки», ядро посылает данный сигнал процессам в группе процессов терминала. Группе процессов, ID которой совпадает с ID управляющего терминала, разрешено записывать в терминал и читать с него. Эта группа называется приоритетной (foreground) группой процессов. (Она получает также генерируемые клавиатурой сигналы.) Любые другие группы процессов в сеансе являются фоновыми (background) группами процессов и не могут читать или записывать в терминал; они получают специальные сигналы, которые их останавливают, если они пытаются это делать. Задания переходят из приоритетного режима в фоновый и обратно не путем изменения атрибута задания, но посредством изменения группы процессов управляющего терминала. Это изменение осуществляет именно контролирующая задания оболочка, и если новая группа процессов останавливается, оболочка вновь запускает ее, посылая сигнал «продолжить» всем членам группы процессов. В былые времена пользователи часто использовали последовательные терминалы, соединенные с модемами, для подключения к централизованным Unix-системам на мини- компьютерах. Когда пользователь закрывал соединение (вешал трубку), линия последовательной передачи обнаруживала отсоединение, и ядро посылало сигнал «отсоединение» всем подключенным к терминалу процессам. Эта концепция остается: если возникает отключение (оборудование последовательной связи все еще существует и все еще используется), ядро посылает сигнал отсоединения приоритетной группе процессов. Если существует лидер сеанса, происходит то же самое. Висячая (orphaned) группа процессов - это такая группа, в которой для каждого процесса родительский процесс находится в той же группе или в другом сеансе. (Это может случиться, если управляющая заданиями оболочка завершается при все еще работающих фоновых заданиях.) Запущенным процессам в висячей группе процессов разрешается работать до завершения. Если в такой группе на момент, когда она становится висячей, уже имеются остановленные процессы, ядро посылает этим процессам сигнал отсоединения, а затем сигнал продолжения. Это заставляет их пробудиться, чтобы они могли завершиться, вместо того, чтобы остаться остановленными навечно. 9.2.2. Идентификация группы процессов: getpgrp () и getpgid () Для совместимости с более старыми системами POSIX предоставляет множество способов получения сведений о группе процессов: #include <unistd.h> pid_t getpgrp(void); /* POSIX */ pid_t getpgid(pid_t pid); /* XSI */ Функция getpgrp () возвращает ID группы процессов текущего процесса, getpgid () является расширением XSI. Она возвращает ID группы процессов для данного pid группы
304 Глава 9. Управление процессами и каналы процессов, pid, равный 0, означает «группа процессов текущего процесса». Таким образом, 'getpgid(O)' является тем же самым, что и 'getpgrp() \ При обычном программировании следует использовать getpgrp (). В BSD 4.2 и 4.3 также есть функция getpgrpO, но она действует как функция POSIX getpgid(), требуя аргумент pid. Поскольку современные системы поддерживают POSIX, в новом коде следует использовать версию POSIX. (Если вы думаете, что это сбивает с толку, вы правы. Нескодько способов для получения одного и того же результата является обычным итогом проектирования комитетом, поскольку комитет считает, что он должен удовлетворить каждого.) 9.2.3. Установка группы процесса: setpgid () и setpgrp () Две функции устанавливают группу процесса: #include <unistd.h> int setpgid(pid_t pid, ,pid_t pgid) ; /.* POSIX */ int setpgrp(void); /* XSI */ ' • Функция setpgrp () проста: она устанавливает ID группы процесса равной ID процесса. Это создает новую группу процессов в том же сеансе, а вызывающий функцию процесс становится лидером группы процессов. Функция setpgid () предназначена для использования управления заданиями. Она позволяет одному процессу устанавливать группу процесса для другого. Процесс может изменить лишь свой собственный ID группы процессов или ID группы процессов порожденного процесса, лишь если этот порожденный процесс не выполнил еще exec. Управляющая заданиями оболочка делает этот вызов после fork как в родительском, так и в порожденном процессах. Для одного из них вызов завершается успехом, и ID группы процессов изменяется. (В противном случае нет способа гарантировать упорядочение, когда родитель может изменить ID группы процессов порожденного процесса до того, как последний выполнит exec. Если сначала успешно завершится вызов родителя, он может перейти на следующую задачу, такую, как обработка других заданий или управление терминалом.) При использовании setpgid () pgid должна быть группой существующего процесса, которая является частью текущего сеанса, фактически подключая pid к этой группе процессов. В противном случае pgid должна равняться pid, создавая новую группу процессов. Имеется несколько значений для особых случаев как для pid, так и для pgid: pid =0 В данном случае setpgid () изменяет группу процессов вызывающего процесса на pgid. Это эквивалентно 'setpgid (getpid() , pgid)'. pgid = 0 Это устанавливает ID группы процессов для данного процесса равным его PID. Таким образом, 'setpgid(pid/ 0)' является тем же самым, что и'setpgid(pid, pid)'. Это делает процесс с PID, равным pid, лидером группы процессов.
9.3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO 305 Во всех случаях лидеры сеанса являются особыми; их PID, ID группы процессов и ID сеанса идентичны, a ID группы процессов лидера не может быть изменена. (ID сеанса устанавливаются посредством setsidO, а получаются посредством getsid(). Это особые вызовы: см. справочные страницы setsid{2) и getsid{2)). 9.3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO Меж:процессное взаимодействие (Interprocess communication - IPC) соответствует своему названию: это способ взаимодействия для двух отдельных процессов. Самым старым способом IPC на системах Unix является канал (pipe): односторонняя линия связи. Данные, записанные в один конец канала, выходят из другого конца. 9.3.1. Каналы Каналы проявляют себя как обычные дескрипторы файлов. Без особого разбирательства вы не можете сказать, представляет ли дескриптор файла сам файл или канал. Это особенность; программы, которые читают из стандартного ввода и записывают в стандартный вывод, не должны знать или заботиться о том, что они могут взаимодействовать с другим процессом. Если хотите знать, каноническим способом проверки этого является попытка выполнить с дескриптором 'lseek (f d, OL, SEEK_CUR)'; этот вызов пытается отсчитать 0 байтов от текущего положения, т. е. операция, которая ничего не делает8. Эта операция завершается неудачей для каналов и не наносит никакого вреда другим файлам. 9.3.1.1. Создание каналов Системный вызов pipe () создает канал: #include <unistd.h> /* POSIX */ int pipe(int filedes[2]); Значение аргумента является адресом массива из двух элементов целого типа, pipe () возвращает 0 при успешном возвращении и -1, если была ошибка. Если вызов был успешным, у процесса теперь есть два дополнительных открытых дескриптора файла/Значение filedes[0] является читаемым концом канала, a f iledes [l] - записываемым концом. (Удобным мнемоническим способом запоминания является то, что читаемый конец использует индекс 0, аналогичный дескриптору стандартного ввода 0, а записываемый конец использует индекс 1, аналогичный дескриптору стандартного вывода 1.) Как упоминалось, данные, записанные в записываемый конец, считываются из читаемого конца. После завершения работы с каналом оба конца закрываются с помощью Такая операция часто обозначается no-op - «no operation» (нет операции). - Примеч. автора.
306 Глава 9. Управление процессами и каналы вызова close (). Следующая простая программа, ch09-pipedemo. с, демонстрирует каналы путем создания канала, записи в него данных, а затем чтения этих данных из него: I /* ch09-pipedemo.c демонстрация ввода/вывода с каналом. */ 2 3 #include <stdio.h> 4 #include <errno.h> 5 #include <unistd.h> 6 7 /* main создание канала, запись в него и чтение из него. */ 8 9 int main(int argc, char **argv) 10- { II static .onst char mesgU = "Don't Panic!"; /* известное сообщение */ 12 char buf[BUFSIZ]; 13 ssize__t rcount, wcount; 14 int pipefd[2]; 15 size__t 1; 16 17 if (pipe(pipefd) < 0) { 18 fprintf(stderr, "%s: pipe failed: %s\n"/ argv[0] , 19 strerror(errno)); 20 exit(l); 21 } " 22 23 prlntf("Read end = fd %d, write end = fd %d\n", 24 pipefd[0], pipefd[1]); 25 . 26 1 = strlen(mesg); 27 if ((wcount = write(pipefd[l], mesg, 1)) != 1) { 28 fprintf(stderr, "%s: write failed: %s\n", argv[0], 29 strerror(errno)); 30 exit(l); 31 } 32 33 if ((rcount = read(pipefd[0], buf, BUFSIZ)) != wcount) { 34 fprintf(stderr, "%s: read failed: %s\n", argv[0], 3 5 strerror(errno)); 36 exit(l); 37 } 38 39 buf[rcount] = ' \0'; . 40 41 printf("Read <%s> from pipe\n", buf);
9,3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO 307 42 (void) close(pipefd[0]); 43 (void) . close<pipefd[l]); 44 45 return 0; 46 } Строки 11-15 объявляют локальные переменные; наибольший интерес представляет mesg, который представляет текст, проходящий по каналу. Строки 17-21 создают канал с проверкой ошибок; строки 23-24 выводят значения новых дескрипторов файлов (просто для подтверждения, что они не равны 0, 1 или 2). В строке 26 получают длину сообщения для использования с write (). Строки 27-31 записывают сообщение в канал, снова с проверкой ошибок. Строки 33-37 считывают содержимое канала,юпять с проверкой ошибок. Строка 39 предоставляет завершающий нулевой байт, так что прочитанные данные могут использоваться в качестве обычной строки. Строка 41 выводит данные, а строки 42-43 закрывают оба конца канала. Вот что происходит при запуске программы: $ ch09-pipedemo Read end = fd 3, write end = fd 4 Read <Don•t Panic!> from pipe Эта программа не делает ничего полезного, но она демонстрирует основы. Обратите внимание, что нет вызовов open () или creat () и что программа не использует три своих унаследованных дескриптора. Тем не менее, write () и read() завершаются успешно, показывая, что дескрипторы файлов действительны и что данные, поступающие в канал, действительно выходят из него9. Конечно, будь сообщение слишком большим, наша программа не работала бы. Это происходит из-за того, что размер (памяти) каналов ограничен, факт, который мы обсудим в следующем разделе. Подобно другим дескрипторам файлов, дескрипторы для каналов наследуются порожденным процессом после fork, и если они не закрываются, все еще доступны после exec. Вскоре мы увидим, как использовать это обстоятельство и сделать ? каналами что-то интересное. 9.3.1.2. Буферирование каналов Каналы буферируют свои данные, что означает, что записанные в канал данные хранятся ядром до тех пор, пока не будут прочитаны. Однако, канал может содержать лишь такое-то количество записанных, но еще не прочитанных данных. Мы можем называть записывающий процесс производителем, а читающий процесс потребителем. Как система управляет полными и пустыми каналами? Когда канал полон, система автоматически блокирует производителя в следующий раз, когда он пытается осуществить запись данных в канал с помощью write (). Когда Мы уверены, что вы не волновались. В конце концов, вы, возможно, используете конвейеры из оболочки десятки раз в день. - Примеч. автора.
308 Глава 9, Управление процессами и каналы канал освобождается, система копирует данные в канал, а затем позволяет системному вызову write () вернуться к производителю. Подобным же образом, если канал пустой, потребитель блокируется в read () до тех пор, пока в канале не появятся данные для чтения. (Блокирующее поведение можно отключить; это обсуждается в разделе 9.4.3.4 «Неблокирующий ввод/вывод для каналов и очередей FIFO».) Когда производитель вызывает на записывающем конце канала close (), потребитель может успешно прочесть любые данные, все еще находящиеся в канале. После этого дальнейшие вызовы read () возвращают 0, указывая на конец файла. Напротив, если потребитель закрывает читаемый конец, write () на записываемом конце завершается неудачей. В частности, ядро посылает производителю сигнал «нарушенный канал», действием по умолчанию для которого является завершение процесса. Нашей любимой аналогией для каналов является то, как муж и жена вместе моют и сушат тарелки. Один супруг моет тарелки, помещая чистые, но влажные тарелки в сушилку на раковине. Другой супруг вынимает тарелки из сушилки и вытирает их. Моющий тарелки является производителем, сушилка является каналом, а вытирающий является потребителем10. Если вытирающий супруг оказывается быстрее моющего, сушилка становится пустой, и вытирающему приходится ждать, пока не будут готовы новые тарелки. Напротив, если быстрее вытирающий супруг, сушилка наполняется, и моющему приходится ждать, пока она не опустеет, прежде чем помещать в нее тарелки. Это изображено на рис. 9.3. 9.3.2. Очереди FIFO Для традиционных каналов единственным способом для двух различных программ получить доступ к одному и тому же каналу является наследование дескрипторов файлов. Это означает, что процессы должны быть порожденными от общего родителя или один должен быть предком другого. Что они ели на обед, остается не указанным. - Примеч. автора.
9.3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO 309 Это может быть серьезным ограничением. Многие системные службы запускаются как демоны, отсоединенные долгоживущие процессы. Должен быть способ отправки данных таким процессам (и, возможно, получения данных от них). Файлы для этого не подходят; синхронизация трудна или невозможна, а каналы для выполнения задания не могут быть созданы, поскольку нет общих предков. Для решения этой проблемы System III предложила идею о FIFO. FIFOl\ или именованный канал, является файлом в файловой системе, который действует подобно каналу. Другими словами, один процесс открывает FIFO для записи, тогда как другой открывает его для чтения. Затем данные, записанные; в FIFO, читаются читателем. Данные буферируются ядром, а не хранятся на диске. Рассмотрите спулер печати. Демон спулера управляет физическими принтерами, создавая задания для печати и печатая по одному заданию за раз. Для добавления в очередь задания программное обеспечение принтера на уровне пользователя должно сообщаться с демоном спулера. Одним способом для осуществления этого является создание спулером FIFO с хорошо известным именем файла. Программа пользователя может затем открыть FIFO, записать в него запрос и снова закрыть. Спулер находится в цикле, читая запросы из FIFO и обрабатывая их. Функция mkf if о () создает файлы FIFO: #include <sys/types.h> /* POSIX */ #include <sys/stat.h> int mkf if о (const chair *pathname/ mode_t mode); Аргумент pathname является именем создаваемого FIFO, a mode является данными ему правами доступа, аналогичными второму аргументу функции creat () или третьему аргументу функции open () (см. раздел 4.6 «Создание файлов»). Файлы FIFO удаляются, как любые другие, с помощью remove () или unlink () (см. раздел 5.1.5.1 «Удаление открытых файлов»). Справочная страница GNU/Linux mkfifoC) указывает, что FIFO должен быть открыт как для чтения, так и для записи в одно и то же время, до того, как может быть осуществлен ввод/ вывод: «Открытие FIFO для чтения обычно блокирует до тех пор, пока какой-нибудь другой процесс не откроет тот же FIFO для записи, и наоборот». После открытия файла FIFO он действует подобно обычному каналу; т. е. это просто еще один дескриптор файла. Команда mkf if о доставляет этот системный вызов на командный уровень. Это упрощает показ файла FIFO в действии: $ mkfifо afifо /* Создание файла FIFO */ $ Is -l afifo /* Показать тип и права доступа, обратите внимание на *р' впереди */ prw-r—г— 1 arnold devel 0 Oct 23 15:49 afifo $ cat < afifo & /* Запустить читателя в фоновом режиме */ [1] 22100 FIFO означает «first in, fist out» - «первым вошел, первым вышел». Так работают каналы. - Примеч. автора
310 Глава 9. Управление процессами и каналы $ echo It was a Blustery Day > afifo /* Послать данные в FIFO */ $ It was a Blustery Day /* Приглашение оболочки, cat выводит данные */ /* Нажмите ENTER, чтобы увидеть статус завершения задания */ [1]+ Done cat <afifo /* cat завершился */ 9.4. Управление дескрипторами файлов На данный момент части загадки почти полностью составлены, fork () и exec () создают процессы и запускают в них программы, pipe () создает канал, который может использоваться для IPC. Чего до сих пор не хватает, так это способа помещения дескрипторов канала на место стандартных ввода и вывода для производителя и потребителя канала. Системные вызовы dup () и dup2 (), совместно с close () дают вам возможность поместить (скопировать) открытый дескриптор файла на другой номер. Системный вызов f cntl () дает вам возможность то же самое и управлять несколькими важными атрибутами открытых файлов. 9.4.1. Дублирование открытых файлов: dup () и dup2 () Два системных вызова создают копию открытого дескриптора файла: #include <unistd.h> /* POSIX */ , ¦ int dup(int oldfd); int dup2(int oldfd, int newfd); Функции следующие: int dup(int oldfd) Возвращает наименьшее значение неиспользуемого дескриптора файла; это копия pldf d. dup() возвращает неотрицательное целое в случае успеха и-1 при неудаче. int dup2(int oldfd, int newfd) Делает newfd копией oldfd; если newfd открыт, он сначала закрывается, как при использовании close (). dup2 () возвращает новый дескриптор или -1, если была проблема. Помните рис. 9.1, в котором два процесса разделяли общие указатели на один и тот же элемент файла в таблице файлов ядра? dup () и dup2 () создают ту же ситуацию внутри одного процесса. См. рис. 9.4. На этом рисунке процесс выполнил 4dup2 A, 3)', чтобы сделать дескриптор файла 3-й копией стандартного вывода, дескриптора файла 1. Точно как описано ранее, эти два дескриптора разделяют общее смещение открытого файла. В разделе 4.4.2 «Открытие и закрытие файлов» мы упомянули, что open () (и creat ()) всегда возвращают наименьшее целое значение неиспользуемого дескриптора для открываемого файла. Этому правилу следуют почти все системные вызовы, которые возвращают новые дескрипторы файлов, а не только open () и creat (). (dup2 () является исключением, поскольку он предусматривает способ получения конкретного нового дескриптора файла, даже если он не является наименьшим неиспользуемым дескриптором.)
9.4. Управление дескрипторами файлов 311 PID 42 Таблицы дескрипторов, на один процесс Г Г Г Г О [1 [ 2 Г 3 [ г ft > г f г Учет ресурсов [Учет ресурсов [ Учет ресурсов | файлов: Смещение Содержание файла: [ Смещение | Смещение | т т% Г Г Г Г [ Г Рис. 9.4. Разделение дескриптора файла как результат *dup2(l,3)' При наличии правила «возвращения наименьшего неиспользуемого номера» в сочетании с функцией dup () теперь легко поместить дескрипторы файла канала на место стандартного ввода и вывода. В предположении, что текущим процессом является оболочка и что ей необходимо создать два порожденных процесса для образования двухступенчатого канала, вот эти шаги: 1. Создать канал с помощью pipe (). Это должно быть сделано сначала, чтобы два порожденных процесса могли унаследовать дескрипторы открытых файлов. 2. Создать то, что мы называем «левым потомком». Это процесс, стандартный вывод которого идет в канал. В данном процессе сделать следующее: a. Использовать 'close (pipefd[0] )', поскольку читаемый конец канала в левом потомке не нужен. b. Использовать 'close A)', чтобы закрыть первоначальный стандартный вывод. c. Использовать 'dup(pipefd[1])' для копирования записываемого конца канала в дескриптор файла 1. d. Использовать 'close (pipef d [ 1 ] )', поскольку нам не нужны две копии открытого дескриптора. e. Выполнить exec для запускаемой программы. 3. Создать то, что мы называем «правым потомком». Это процесс, стандартный ввод которого поступает из канала. Шаги для этого потомка являются зеркальным отражением шагов для левого потомка: a. Использовать 'close (pipef d[ 1] )', поскольку записываемый конец канала в правом потомке не нужен. b. Использовать 'close @)', чтобы закрыть первоначальный стандартный ввод.
312 Глава 9. Управление процессами и каналы c. Использовать 'dup(pipefd[0] )' для копирования читаемого конца канала в дескриптор файла 0. d. Использовать 'close (pipef d [ 0 ] )', поскольку нам не нужны две копии открытого дескриптора. e. Выполнить exec для запускаемой программы. 4. В родителе закрыть оба конца канала: 'close (pipef d[0] ) ; close (pipef d[l])'. 5. Наконец, использовать в родителе wait() для ожидания завершения обоих порожденных процессов. Обратите внимание, как важно закрыть неиспользуемые копии дескрипторов файлов каналов. Как мы отмечали ранее, файл не закрывается до тех пор, пока не будет закрыт последний открытый для него дескриптор. Это верно, даже если дескрипторы файлов разделяют несколько процессов. Закрытие не использующихся дескрипторов файлов имеет значение, поскольку процесс, читающий из канала, не получит указания конца файла, пока все копии записываемого конца не будут закрыты. В нашем случае после порождения двух потомков имеются три процесса, у каждого из которых есть копии двух дескрипторов файлов каналов: родительский и два порожденных. Родительский процесс закрывает оба конца, поскольку ему не нужен канал. Левый потомок записывает в канал, поэтому ему нужно закрыть читаемый конец. Правый потомок читает из канала, поэтому ему нужно закрыть записываемый конец. Это оставляет открытым ровно по одной копии дескриптора файла. Когда левый потомок завершает работу, он заканчивается. Система после этого закрывает все его дескрипторы файлов. Когда это случается, правый потомок получает в конечном счете уведомление конца файла и тоже может завершить работу и выйти. Следующая программа, chO9-pipeline.с, создает эквивалент следующего конвейера оболочки: $ echo hi there | sed s/hi/hello/g hello there Вот программа: 1 /¦* chO9-pipeline.с ответвляет два процесса в их собственный конвейер. 2 Для краткости проверка ошибок сведена к минимуму. */ 3 4 #include <stdio.h> 5 #include <errno.h> 6 #include <sys/types.h> 7 #include <sys/wait.h> 8 #include <unistd.h> 9 10 int pipefd[2]; 11 12 extern void left__child(void) , right_child(void) ; 13
9.4. Управление дескрипторами файлов 313 14 /* main порождение процессов и ожидание их завершения */. 15 16 int main(int argc, char **argv) 17 { 18 pid_t left_pid/ right_pid; 19 pid_t ret; 20 int status; 21 22 if (pipe(pipefd) < 0) { /* создать канал в самом начале */ 23 perror("pipe"); 24 exit(l); 25 } 26 27 if ((left_pid = fork()) < 0) { /* порождение левого потомка */ 2 8 perror("fork"); 29 exit(l); 30 } else if (left_pid == 0) 31 left_child(); 32 3 3 if ((right__pid = fork()) < 0) { /* порождение правого потомка */ 34 perror("fork"); 3 5 * exitA); 36 } else if (right_pid ==0) 37 right_child(); 38 39 close(pipefd[0]); /* закрыть родительские копии канала */ 40 close(pipefd[l]); 41 42 while ((ret = wait(& status)) > 0) { /* wait for children */ 43 if (ret == left_pid) 44 printf("left child terminated,•status: %x\n"/ status); 45 else if (ret == right__pid) 46 printf("right child terminated, status: %x\n", status); 47 else 48 printf("yow! unknown child %d terminated, status %x\n", 49 ret, status); 50 } 51 52 return 0; 53 } Строки 22-25 создают канал. Это должно быть сделано в самом начале. Строки 27-31 создают левого потомка, а строки 33-37 создают правого потомка. В обоих случаях родитель продолжает линейное исполнение ветви main () до тех пор, пока порожден-.
314 Глава 9. Управление процессами и каналы ный процесс не вызовет соответствующую функцию для манипулирования дескрипторами файла и осуществления exec. Строки 39-40 закрывают родительскую копию канала. Строки 42-50 в цикле ожидают потомков, пока wait () не вернет ошибку. 55 /*¦ left_child осуществляет работу левого потомка */'. 56 57 void left_child<void) 58 { 59 static char *left_argv[] = { "echo", "hi", "there", NULL }; 60 61 close(pipefd[0]); 62 closed) ; 63 dup(pipefd[l]); 64 close(pipefd[l]); 65 66 execvp("echo", left_argv); 67 _exit(errno == ENOENT ? 127 : 126); 68 } 69 70 /* right__child осуществляет работу правого потомка */ 71 72 void right_child(void) 73 { 74 static char *right_argv[] = { "sed", "s/hi/hello/g", NULL }; 75 • * 76 close(pipefd[l]); 77 close(O); 7 8 dup(pipefd[0]); 79 close(pipefd[0]); 80 81 execvp("sed", right_argv); 82 _exit(errno == ENOENT ? 127 : 126); 83 } Строки 57-68 являются кодом для левого потомка. Процедура следует приведенным выше шагам, закрывая ненужный конец канала, закрывая первоначальный стандартный вывод, помещая с помощью dup () записываемый конец канала на номер 1 и закрывая затем первоначальный записываемый конец. В этот момент строка 66 вызывает execvpO, и если она завершается неудачей, строка 67 вызывает _exit(). (Помните, что строка 67 никогда не выполняется, если execvp () завершается удачно.) Строки 72-83 делают подобные же шаги для правого потомка. Вот что происходит при запуске: $ ch09-pipeline /* Запуск программы */ left child terminated, status: 0 /* Левый потомок завершается до вывода (!) */ hello there /* Вывод от правого потомка */
9.4. Управление дескрипторами файлов 315 right child terminated, status: 0 $ ch09-pipeline /* Повторный запуск программы */ hello there /* Вывод от правого потомка и ... */ right child terminated, status: 0 /* Правый потомок завершается до левого */ left child terminated, status: 0 Обратите внимание, что порядок, в котором завершаются потомки, не является детерминированным. Он зависит от загрузки системы и многих других факторов, которые могут повлиять на планирование процессов. Вам следует проявить осторожность, чтобы избежать предположений о порядке действий при написании кода, создающего несколько процессов, в особенности для кода, который вызывает семейство функций wait (). Весь процесс показан на рис. 9.5. На рис. 9.5 (а) изображена ситуация после создания родителем канала (строки 22-25) и двух порожденных процессов (строки 27-37). На рис. 9.5 (Ь) показана ситуация после закрытия родителем канала (строки 39-40) и начала ожидания порожденных процессов (строки 42-50). Каждый порожденный процесс поместил канал на место стандартного вывода (левый потомок, строки 61-63) и стандартного ввода (строки 76-78). Наконец, рис. 9.5 (с) изображает ситуацию после закрытия потомками первоначального канала (строки 64 и 79) и вызова execvp () (строки 66 и 81). 9.4.2. Создание нелинейных конвейеров:/dev/fd/xx Многие современные системы Unix, включая GNU/Linux, поддерживают в каталоге /dev/fd 12 специальные файлы. Эти файлы представляют дескрипторы открытых файлов с именами /dev/fd/0, /dev/fd/1 и т. д. Передача такого имени функции open () возвращает новый дескриптор файла, что в сущности является тем же самым, что и вызов dup () для данного номера дескриптора. Эти специальные файлы находят свое применение на уровне оболочки: Bash, ksh88 (некоторые версии) и ksh93 предоставляют возможность замещения процесса (process substitution), что позволяет создавать нелинейные конвейеры. На уровне оболочки для входного конвейера используется запись 4< (...) Уа для выходного конвейера запись '> (...)\ Например, предположим, вам нужно применить команду dif f к выводу двух команд. Обычно вам пришлось бы использовать временные файлы: commandl > /tmp/out.$$.1 coiranand2 > /tmp/out .$$ .2 diff /tmp/out.$$.1 /tmp/out.$$.2 rm /tmp/out.$$.1 /tmp/out.$$.2 С замещением процессов это выглядит следующим образом: / diff <(commandl) <(command2) 12 На системах GNU/Linux /dev/ f d является символической ссылкой на /proc/self / f d, но поскольку /dev/ f d является общеизвестным, в своем коде следует использовать именно его. - Примеч. автора.
316 Глава 9. Управление процессами и каналы (а) ch'09-pipeline |— pipeQ » f d 3 fork () fork () / \ ch09-pipeline ch09-pipeline J fd 4 Канал разделяется между родителем и обоими потомками (Ь) й потомок Правый потомок ch09-pipellne L close () -> J-—\ wait () wait () fd^3 «J Родитель закрывает канал ожидает потомков / Л ch09-pipeline | ^ close () /dup () -4^ close () /dup () J ch09-pipeline Левый потомок Правый потомок Потомку помещают канал в стандартный вывод (левый потомок) и стандартный ввод (правый потомок), закрывая первоначальный канал (с) ch09-pipeline § wait () wait () / \ Потомки вызывают exec (), программы запускаются echo hi there f » 1 г 0 jj » sed s/hi/hello/g § hello there execvpO Левый потомок execvp() Канал Правый потомок Рис. 9.5. Создание конвейера родителем
9.4. Управление дескрипторами файлов 317 Не надо никаких беспорядочных файлов для временного запоминания и удаления. Например, следующая команда показывает, что наш домашний каталог является ссылкой на другой каталог: $ diff <(pwd) <(/bin/pwd) lcl < /home/arnold/work/prenhall/progex > /d/home/arnold/work/prenhall/progex Незамысловатая команда pwd является встроенной в оболочку: она выводит текущий логический путь, который управляется оболочкой с помощью команды cd. Программа /bin/pwd осуществляет обход физической файловой системы для вывода имени пути. Как выглядит замещение процессов? Оболочка создает вспомогательные команды13 ('pwd' и Vbin/pwd'). Выход каждой из них подсоединяется к каналу, причем читаемый конец открыт в дескрипторе нового файла для главного процесса ('diff'). Затем оболочка передает главному процессу имена файлов в /dev/fd в качестве аргументов командной строкилМы можем увидеть это, включив в оболочке трассировку исполнения: $ set -х /* Включить трассировку исполнения */ $ diff <(pwd) <(/bin/pwd) /* Запустить команду */ + diff /dev/fd/63 /dev/fd/62 /* Трассировка оболочки: главная/ программа, обратите внимание на аргументы */ ++ pwd /* Трассировка оболочки: вспомогательные программы */ ++ /bin/pwd lcl /* Вывод diff */ < /home/arnold/work/prenhall/progex > /d/home/arnold/work/prenhall/progex Это показано на рис. 9,6. Если на вашей системе есть /dev/fd, вы также можете использовать преимущества этой возможности. Однако, будьте осторожны и задокументируйте то, что вы делаете. Манипуляции с дескриптором файла на уровне С значительно менее прозрачны, чем соответствующие записи оболочки! 9.4.3. Управление атрибутами файла: fcntl О Системный вызов fcntl () («управление файлом») предоставляет контроль над различными атрибутами либо самого дескриптора файла, либо лежащего в его основе открытого файла. Справочная страница GNU/Linux/c«//B) описывает это таким способом: #include <unistd.h> /* POSIX **/ #include <fcntl.h> Хотя мы показали простые команды, допустимы произвольные конвейеры. - Примеч. автора.
№ Глава 9. Управление процессами и каналы pwd /bin/pwd fd 1 \ fd 63 Канал Канал diff /de\r/fd/63 /dev/fd/62 fd 1 \ fd 62 Рис. 9.6. Замещение процесса int fcntl(int fd, int cmd) ; int fcntl(int fd, int cmd, long arg) ; int fcntl(int fd, int cmd, struct flock *lock); Другими словами, функция принимает по крайней мере два аргумента; в зависимости от второго аргумента, она может принимать и третий аргумент. Последняя форма, в которой третий аргумент является указателем на struct flock, предназначена для блокировки файла. Блокировка файлов сама по себе представляет большую тему; мы отложим обсуждение до раздела 14.2 «Блокировка файлов». 9.4.3.1. Флагclose-on-exec После вызова f ork() и перед вызовом exec () следует убедиться, что новая программа наследует лишь те открытые файлы, которые ей нужны. Вы не захотите, чтобы порожденный процесс мешался в открытых файлах родителя, если только это так не задумано. С другой стороны, если у родителя множество открытых файлов, это будет искусственно ограничивать число новых файлов, которые может открыть порожденный процесс. (См. сопроводительную врезку.) Организационно такое поведение может представлять проблему. Часть вашей программы, порождающая новый процесс, не должна особенно нуждаться в других частях программы, манипулирующей открытыми файлами. И цикл наподобие следующего неприятный, поскольку может не быть открытых файлов: int j; , for (j = getdtablesizeO ; j >= 3; j —) /* закрыть все, кроме 0, 1, 2 */ (void) close!j); Решением является флаг close-on-exec (закрытие при исполнении exec). Он является атрибутом самого дескриптора файла, а не лежащего в его основе открытого файла. Когда этот флаг установлен, система автоматически закрывает файл, когда процесс осуществляет exec. Установив этот флаг сразу после открытия файла, вам не нужно беспокоиться о том, что какой-нибудь порожденный процесс случайно его унаследует. (Оболочка автоматически устанавливает этот флаг для всех дескрипторов файлов, которые она открывает, начиная с номера 3 и выше.)
9.4. Управление дескрипторами файлов 319 Аргумент cmd имеет два значения, относящиеся к флагу close-on-exec: F_GETFD Получает флаги дескриптора файла. Возвращаемое значение является значением всех установленных флагов дескриптора или -1 при ошибке. F_SETFD Устанавливает флаги дескриптора файла в содержащееся в arg (третий аргумент) значение. Возвращаемое значение равно 0 при успехе или -1 при ошибке. В настоящий момент определен лишь один «флаг дескриптора файла»: FD_CLOEXEC. Эта именованная константа является нововведением POSIX14, а большая часть кода использует просто 1 или 0: if (fcntMfd, F_SETFD, 1) < 0) ... /* установить close-on-exec, обработать ошибки */ if (fcntl(fd, F_GETFD) == 1), ... /* бит close-on-exec уже установлен */ Однако, определение POSIX допускает дальнейшее расширение, поэтому правильный способ написания такого кода больше соответствует этим строкам: int fd; long fd__flags; if ((fd__flags = fcntMfd, FJ3ETFD) ) < 0) /* Получить флаги */ /* обработать ошибки */ fd__flags | = FD_CLOEXEC; Add close-on-exec flag if (fcntMfd, F_SETFD, fd_flags) < 0) /* Установить флаги.*/ /* обработать ошибки */ ^ЗАМЕЧАНИЕ. Флаг close-on-exec является собственностью дескриптора, а не Хлежащего в его основе файла. Поэтому новый дескриптор, возвращенный функ- Хциями dup () или dup2 () (или fcntlf) с F_DUPD, которую мы намереваемся I посмотреть), не наследует установки флага close-on-exec первоначального дескрип- I тора. Если вам нужно установить его также и для нового дескриптора файла, вы I должны не забыть сделать это сами. Такое поведение имеет смысл: если вы про- \сто вызвали dup О, копируя один конец канала в 0 или 1, вы не захотите, чтобы J система закрыла его вместо вас, как только процесс осуществит exec! История борьбы close-on-exec от gawk ; В языке awk операторы ввода/вывода используют обозначение перенаправления, сходное с обозначением для оболочки. Это включает односторонние каналы к и от подпроцесса: print "something brilliant" > "/some/file" /•* Вывод в файл */ getline my_record < "/some/other/file /* Ввод из файла */ print "more words of wisdom" | "a_reader process" /* Вывод в подпроцесс */ "a_writer process" | getline some_input /* Ввод из подпроцесса */ Стандарт POSIX умышленно не приписывает ей значение. Однако, чтобы старый код продолжал работать, единственным значением, которое могла бы разумно использовать любая реализация, является 1. - Примеч. автора.
320 Глава 9. Управление процессами и каналы У интерпретатора awk есть дескрипторы открытых файлов для всех перенаправлений файлов, а для обозначений каналов, создающих подпроцессы, интерпретатор awk создает канал, а затем осуществляет fork и exec оболочки для запуска команды, приведенной в строке. Теперь на современных системах часть стартового кода библиотеки С времени исполнения (который запускается до вызова main ()) нуждается для управления использованием разделяемых библиотек во временно открытых файлах. Это означает, что для новой программы после исполнения exec должны быть по крайней мере один или два неиспользуемых дескрипторе файла, иначе программа просто не будет работать. Однажды один пользователь сообщил, что когда в программе ^wk было максимальное количество открытых файлов, ни один процесс, для которого она пыталась использовать для конвейера fork и exec, не мог успешно начаться! Вы, возможно, можете догадаться, что произошло. Порожденная оболочка унаследовала дескрипторы открытых файлов, которые gawk сама использовала для своих перенаправлений. Мы модифицировали gawk так, чтобы установить флаг close-on-exec для всех перенаправлений файлов и каналов, что и решило проблему. 9.4.3.2. Дублирование дескриптора файла Когда аргумент cmd функции f cntl () равен F_DUPFD, ее поведение похоже, но не идентично поведению dup2 (). В этом случае arg является дескриптором файла, представляющим наименьшее приемлемое значение для нового дескриптора файла: int new_fd = fcntl(old_fd, F__DUPFD, 7); /* Возвращаемое значение между 7 и максимумом или неудача */ int new_fd = dup2(old_fd, 7); /* Возвращаемое значение 7 или неудача */ Вы можете имитировать поведение dup (), которая возвращает наименьший свободный дескриптор файла, использовав 'f cntl (old_fd, F_DUPED, 0)'. Если вы помните, что дескрипторы файлов являются просто индексами внутренней таблицы, работа этой функции должна быть ясна. Третий аргумент просто предоставляет индекс, с которого ядро должно начать поиск неиспользуемого дескриптора файла. Использовать ли в собственном коде f cntl () с F_DUPED или dup () или dup2 (), в значительной степени является делом вкуса. Все три функции API являются частью POSIX и широко поддерживаются. У нас легкое пристрастие к dup () и dup2 (), поскольку они более специфичны в своих действиях, поэтому являются самодокументирующимися. Но поскольку все они довольно просты, эта аргументация может вас не убедить. 9.4.3.3. Работа с флагами статуса файла и режимами доступа 8 разделе 4.6.3 «Возвращаясь к open ()» мы предоставили полный список флагов 0_хх9 которые принимает open(). POSIX разбивает их по функциям, классифицируя в соответствии с табл. 9.4.
9.4. Управление дескрипторами файлов 321 Таблица 9. 4. Флаги 0_хх для open (), creat () и f cnt 1 () Категория Функции Флаги Доступ к файлу open ( ) , f cntl () 0_RDONLY, 0_RDWR, 0_WRONLY Создание файла open () 0_CREAT, 0_EXCL, 0_NOCTTY, 0_TUNC Статус файла open () , f cnt 1 () 0__APPEND, 0_DSYNC, 0_NONBLOCK, OJRSYNC, 0_SYNC Помимо первоначальной установки различных флагов с помощью open (), вы можете использовать f cntl () для получения текущих установок, а также их изменения. Это осуществляется с помощью значений cmd F_GETFL и F_SETFL соответственно. Например, вы можете использовать эти команды для изменения установки неблокирующего флага, 0_NONBLOCK, подобным образом: int fd_flags; if ((fd_flags = fcntl(fd, F_GETFL)) < 0) /* обработать ошибку */ if ((fd_flags & 0_NONBLOCK) != 0) { /* Установлен неблокирующий флаг */ fd_flags &= ~0_NONBLOCK; /* Сбросить его */ if (fcntl(fd, F_SETFL, fd_flags) != 0) /* Дать ядру новое значение */ /* обработать ошибку */ } Помимо самих режимов именованная константа 0_ACCMODE является маской, которую вы можете использовать для выделения из возвращаемого значения режимов прав доступа: fd_flags = fcntKfd, F_GETFL) ; switch (fd_flags & 0_ACCESS) { case 0_RDONLY: /* . . .действия только для чтения,. . . */ break; case 0_WRONLY: /* ...действия только для записи... */ . break; case 0__RDWR: /* .v.действия для чтения и записи... */ break; } POSIX требует, чтобы 0_RDONLY, 0_RDWR и 0_WRONLY были побитово различными; таким образом, гарантируется, что код, подобный только что показанному, будет работать и является простым способом определения того, как был открыт произвольный дескриптор файла. Используя F_SETFL вы можете также изменить эти режимы, хотя по-прежнему применяется проверка прав доступа. Согласно справочной странице GNU/Linuxybi//B) флаг 0_APPEND не может быть сброшен, если он использовался при открытии файла. 11-159
322 Глава 9. Управление процессами и каналы 9.4.3.4. Неблокирующий ввод/вывод для каналов и FIFO Ранее для описания способа работы каналов мы использовали сравнение с двумя людьми, моющими и вытирающими тарелки с использованием сушилки; когда сушилка заполняется, останавливается моющий, а когда она пустеет, останавливается вытирающий. Это блокирующееповедение: производитель или потребитель блокируются в вызове write () или read (), ожидая либо освобождения канала, либо появления в нем данных. В действительности человек, ожидающий опустения или заполнения сушилки, не должен просто неподвижно стоять15. Вместо этого незанятый супруг мог бы пойти и найти другую работу по кухне (такую, как подметание всех крошек за детьми на полу), пока сушилка снова не будет готова. На языке Unix/^OSIX эта концепция обозначается термином неблокирующий ввод/вывод, т. е. запрошенный ввод/вывод либо завершается, либо возвращает значение ошибки, указывающее на отсутствие данных (для читающего) или отсутствие места (для записывающего). Неблокирующий ввод/вывод применяется к каналам и FIFO, а не к обычным файлам на диске. Он может применяться также и к определенным устройствам, таким как терминалы, и к сетевым соединениям, обе эти темы выходят за рамки данной книги. С функцией open () может использоваться флаг 0_NONBLOCK для указания неблокирующего ввода/вывода, он может быть установлен и сброшен с помощью f cntl (). Для open () и read () неблокирующий ввод/вывод прост. Открытие FIFO с установленным или сброшенным 0_NONBLOCK демонстрирует следующее поведение: open("/fifo/file", OJRDONLY, mode) Блокируется до открытия FIFO для записи, open("/fifo/file", 0_RDONLY | 0_NONBLOCK, mode) Открывает файл, возвращаясь немедленно, open.("/fifo/file", 0_WRONLY, mode) Блокирует до открытия FIFO для чтения. open("/fifo/file", 0_WRONLY | 0_NONBLOCK, mode) Если FIFO был открыт для чтения, открывает FIFO и немедленно возвращается. В противном случае возвращает ошибку (возвращаемое значение -1 и errno установлен в ENXIO). Как описано для обычных каналов, вызов read () для FIFO, который больше не открыт для чтения, возвращает конец файла (возвращаемое значение 0). Флаг 0_NONBLOCK в данном случае неуместен. Для пустого канала или FIFO (все еще открытых для записи, но не содержащих данных) все становится интереснее: read(fd, buf, count) и сброшенный 0_NONBLOCK Функция read () блокируется до тех пор, пока в канал или FIFO не поступят данные, read(fd, buf, count) и установленный 0_NONBLOCK Функция read () немедленно возвращает-1 с установленным в errno EAGAIN. Ну, мы игнорируем мысль, что два супруга могли бы хотеть поговорить друг с другом и насладиться компанией. - Примеч. автора.
9.4. Управление дескрипторами файлов 323 В заключение, поведение write () более сложно. Для обсуждения этого нам нужно сначала представить концепцию атомарной записи. Атомарная запись - это такая запись, при которой все данные записываются целиком, не чередуясь с данными от других записей. POSIX определяет в <unistd. h> константу PIPE_BUF. Запись в канал или FIFO данных размером менее или равным PIPE_BUF байтов либо успешно завершается, либо блокируется в соответствии с подробностями, которые мы скоро приведем. Минимальным значением для PIPE_BUF является __POSIX_PIPE_BUF, что равняется 512. Само значение PIPE_BUF может быть больше; современные системы GLIBC определяют ее размер в 4096, но в любом случае следует использовать эту именованную константу и не ожидать, что PIPE.JBUF будет иметь то же значение на разных системах. Во всех случаях для каналов и FIFO write () добавляет данные в конец канала. Это происходит от того факта, что у каналов нет файловых смещений: в них нельзя осуществлять поиск. Также во всех случаях, как упоминалось, записи размером вплоть до PIPE_BUF являются атомарными: данные не перемежаются с данными от других записей. Данные записи размером более PIPE_BUF байтов могут перемежаться с данными других записей в произвольных границах. Это последнее означает, что вы немоэюете ожидать, что каждая порция размером PIPE__BUF большого набора данных будет записана атомарно. Установка 0_NONBLOCK не влияет на это правило. Как и в случае с read (), когда 0_NONBLOCK не установлен, write () блокируется до тех пор, пока все данные не будут записаны. Наиболее все усложняется, когда установлен 0_NONBLOCK. Канал или FIFO ведут себя следующим образом: размер > nbytes размер < nbytes nbytes < PIPE_BUF write () успешна write () возвращает (-1)/EAGAIN размер > 0 размер = 0 nbytes > PIPE_BUF write () записывает, что write () возвращает может (-1) / EAGAIN Для файлов, не являющихся каналами и FIFO и к которым может быть применен 0_NONBLOCK, поведение следующее: размер > 0 write () записывает, что может размер = 0 , wr i te () возвращает -1 / EAGAIN Хотя есть ряд сбивающих с толку изменений поведения в зависимости от того, канал это или не канал, установлен CLJSTOBLOCK или сброшен, есть в канале место для записи или нет, а также в зависимости от размера предполагаемой записи, эти правила предназначены для упрощения программирования: и*
324 Глава 9. Управление процессами и каналы Всегда можно отличить конец файла: read () возвращает 0 байтов. • Если нет доступных для чтения данных, read () либо завершается успешно, либо возвращает указание «нет данных для чтения»: EAGAIN, что означает «попытайтесь снова позже». Если для записи нет места, write () либо блокируется до успешного завершения (CMSTONBLOCK сброшен), либо завершается неудачей с ошибкой «в данный момент нет места для записи»: EAGAIN. Когда место есть, будет записано столько данных, сколько возможно, так что в конечном счете все данные будут переписаны. Подводя итог, если вы собираетесь использовать неблокирующий ввод/вывод, любой код, который использует write(), должен быть способен обработать укороченную запись, когда успешно записан меньший объем данных, чем было затребовано. Устойчивый код в любом случае должен быть написан таким способом: даже в случае обычного файла диск может оказаться заполненным и write () сможет записать лишь часть данных. Более того, вы должны быть готовы обработать EAGAIN, понимая, что в этом случае неудача write () не обязательно означает фатальную ошибку. То же верно для кода, использующего для чтения неблокирующий ввод/вывод: признайте, что и здесь EAGAIN не является фатальным. (Однако, может стоит подсчитывать число таких отказов, оставив попытки, когда их слишком много.) Неблокирующий ввод/вывод действительно усложняет вашу жизнь, в этом нет никакого сомнения. Но для многих приложений он является необходимостью, позволяющей выполнить задание. Снова рассмотрите спулер печати. Демон спулера не может позволить себе находиться в блокирующем read () для файла FIFO, которому представлены входящие задания. Он должен иметь также возможность отслеживать запущенные задания и, возможно, периодически проверять состояние печатающих устройств (например, убедиться, что не заело бумагу). 9.4.3.5. Сводка fcntl О Сводка для системного вызова fcntl () приведена в табл. 9.5. Таблица 9.5. Сводка fcntl () Значение cmd Значение arg Возвращает F_DUPFD F_GETFD F_SETFD F__GETFL F_SETFL Наименьший новый дескриптор Дублирует аргумент f d. Получает флаги дескриптора файла (close-on-exec). Новое значение флага Устанавливает флаги дескриптора файла (close-on-exec). Получает флаги основного файла. Новое значение флага Устанавливает флаги основного файла. Флаги создания, статуса и прав доступа файла копируются, когда дескриптор файла дублируется. Флаг close-on-exec не копируется.
9.5. Пример: двусторонние каналы в gawk 325 9.5. Пример: двусторонние каналы в gawk Двусторонний канал соединяет два процесса двунаправленным образом. Обычно, по крайней мере для одного из процессов, на канал с другим процессом настраиваются как стандартный ввод, так и стандартный вывод. Оболочка Корна (ksh) ввела двусторонние каналы на уровне языка, обозначив термином сопроцесса (coprocess): команды и аргументы движка базы данных |& /* Запустить сопроцесс в фоновом режиме */ print -p "команда базы данных" /* Записать в сопроцесс */ read -p db__response /* Прочесть из сопроцесса */ Здесь движок базы данных представляет любую серверную программу, которая может управляться интерфейсной частью, в данном случае, сценарием ksh. У движка базы данных стандартный ввод и стандартный вывод подсоединены к оболочке посредством двух отдельных односторонних каналов16. Этр показано на рис. 9.7. . ^ родительская , — print -р rf -^ read -p — оболочка с г у fd N [ fd 0 » движок базы данных' » fd 1 [ fd M •-.*•¦-•!'.-.•.-. ..'*ч: - - ¦*¦¦ •«л- J ¦< it /,.*. «+.tЛ Рис. 9.7. Сопроцессы Канал Канал оболочки Корна В обычном awk каналы к или от подпроцесса являются односторонними: нет способа послать данные в программу и прочесть посланные от нее в ответ данные - нужно использовать временный файл. GNU awk (gawk) заимствует обозначение ' | &' от ksh для расширения языка awk: print "команда" |& "движок базы данных" /* Запустить сопроцесс, записать в него */ "движок базы данных" |& getline db_response /* Прочесть из сопроцесса */ gawk использует запись ' | &' также для сокетов TCP/IP и порталов BSD, которые не рассматриваются в данной книге. Следующий код из io.c в дистрибутиве gawk 3.1.3 является частью функции two_way__open (), которая устанавливает простой сопроцесс: она создает два канала, порождает новый процесс и осуществляет все манипуляции с дескриптором файла. Мы опустили ряд не относящихся к делу частей кода (эта функция занимает больше места, чем следовало бы): 1561 static int. 1562 two_way_open(const char *str, struct redirect *rp) 1563 { В одно и то же время есть только один сопроцесс по умолчанию (доступный посредством 'read -p' и 'print -p'). Сценарии оболочки могут использовать команду exec со специальной записью перенаправления для назначения дескрипторов файла сопроцесса определенным номерам. После этого можно запустить другой сопроцесс. - Примеч. автора.
326 Глава 9. Управление процессами и каналы 1827 /* случай 3: двусторонний канал с порожденным процессом */ 182 8 { 1829 ¦ int' ptoc [2] , ctop[2];. 1830 int pid; 1831 int save_errno; 1835 1836 if (pipe(ptoc) < 0) 1837 return чFALSE; /* установлен errno, диагностика от вызывающего */ 1838 1839 if (pipe(ctop) < 0) { 1840 save_errno = errno; 1841 close(ptoc[0]); 1842 close(ptoc[l]); 184-3 errno = save_errno; 1844 return FALSE; 1845 } Первым шагом является создание двух каналов, ptoc является каналом «от родителя к потомку», а с top - «от потомка к родителю». Во время чтения держите в уме, что индекс 0 является читаемым концом, а 1 - записываемым. Строки 1836-1837 создают первый канал, ptoc. Строки 1839-1845 создают второй канал, закрывая при неудачном создании и первый. Это важно. Небрежность в закрытии открытых, но не используемых каналов ведетк утечкам дескрипторов файлов. Как и память, дескрипторы файлов являются конечным ресурсом, и когда они иссякают, то теряются17. То же верно и для открытых файлов: убедитесь, что ваш обрабатывающий ошибки код всегда закрывает все открытые файлы и каналы, которые не нужны, когда происходит ошибка. save_errno сохраняет значения errno, установленные pipe (), на тот редкий случай, когда close () может завершиться неудачей (строка 1840). Затем errno восстанавливается в строке 1843. 1906 if ((pid = forkO) < 0) { 1907 save__errno = errno; 1908 close(ptoc[0]); close(ptoc[1]); 1909 close(ctop[0]); close(ctop[1]); 1910 err.no = save_errno; 1911 return FALSE; 1912 } Строки 1906-1912 порождают процесс, на этот раз закрывая оба канала, если fork () потерпит неудачу. Здесь также первоначальное значение errno сохраняется и восстанавливается для последующего использования при диагностике. 1914 if (pid ==0) { /* порожденный процесс */ 1915 if (closed) == -1) 1916 fatal(_("close of stdout in child failed (%s)"), Очевидно, вы можете их закрыть. Но если вы не знаете, что они открыты, они теряются с таким же успехом, как и память через утечку памяти. - Примеч. автора.
9.5. Пример: двусторонние каналы в gawk 327 1917 strerror(errno)); ! 1918 if (dup(ctop[l]) != 1) 1919 fatal(_("moving pipe to stdout in child failed (dup: Us)"), strerror(errno)); 1920 if (close(O) == -1) 1921 fatal(_("close of stdin in child failed (%s)"), 1922 strerror(errno)); 1923 if (dup(ptoc[0]) != 0) 1924 fatal(_("moving pipe to stdin in child failed (dup: %s)"), strerror(errno)); 1925 if ( close(ptoc[0]) == -1 || close(ptoc[1]) == -1 1926 || close(ctop[0]) == -1 || close(ctop[l]) == -1) 1927 fatal(_("close of pipe failed (%s)"), strerror(errno)); 192 8 /.* stderr HE дублируется в stdout потомка */ 1929 execl("/bin/sh", "sh", "-с", str, NULL); 1930 _exit(errno == ENOENT ? 127 : 126); 1931 } Строки 1914-1931 обрабатывают код потомка, с соответствующей проверкой ошибок и сообщениями на каждом шагу. Строка 1915 закрывает стандартный вывод. Строка 1918 копирует записываемый конец канала от потомка к родителю на 1. Строка 1920 закрывает стандартный ввод, а строка 1923 копирует читаемый конец канала от родителя к потомку на 0. Если это все работает, стандартные ввод и вывод теперь на месте и подключены к родителю. Строки 1925-1926 закрывают все четыре первоначальные дескрипторы файлов каналов, поскольку они больше не нужны. Строка 1928 напоминает нам, что стандартная ошибка остается на месте. Это лучшее решение, поскольку пользователь увидит ошибки от сопроцесса. Программа awk, которая должна перехватить стандартную ошибку, может использовать в команде обозначение '2>&1' для перенаправления стандартной ошибки сопроцесса или записи в отдельный файл. Наконец, строки 1929-1930 пытаются запустить для оболочки execl () и соответственно выходят, если это не удается. 1934 /* родитель */ 1935 rp->pid = pid; 1936 rp->iop = iop_alloc(ctop[0] ,' str, NULL); 1937 if (rp->iop == NULL) { 1938 (void) close(ctop[0]); 1939 (void) close(ctop[l]); 1940 (void) close(ptoc[0]); 1941 (void) close(ptoc[l]); 1942 (void) kill(pid, SIGKILL); /* overkill? (pardon pun) */ 1943 1944 return FALSE; 1945 }
328 Глава 9. Управление процессами и каналы Первым шагом родителя является настройка входного конца от сопроцесса. Указатель гр указывает на struct redirect, которая содержит поле для сохранения PID порожденного процесса, FILE* для вывода и указатель IOBUF* с именем iop. IOBUF является внутренней структурой данных gawk для осуществления ввода. Она, в свою очередь, хранит копию нижележащего дескриптора файла. Строка 1935 сохраняет значение ID процесса. Строка 1936 выделяет память для новой IOBUF для данных дескриптора файла и командной строки. Третий аргумент здесь равен NULL: он позволяет при необходимости использовать предварительно выделенный IOBUF. Если выделение памяти потерпело неудачу, строки 1937-1942 производят очистку, закрывая каналы и посылая сигнал «kill» порожденным процессам, чтобы заставить их завершить работу. (Функция kill() описана в разделе 10.6.7 «Отправка сигналов: kill () nkillpgO».) 1946 rp->fp = fdopen(ptoc[l] , V); 1947 if (rp->fp == NULL) { 1948 iop__clo*se (rp->iop) ; 1949 rp~>iop = NULL; 1950 (void) close(ctop[0]) 1951 (void) close(ctop[l]) 1952 (void) close(ptoc[0]) 1953 (void) close(ptoc[l]) 1954 (void) kill(pid/ SIGKILL); /* избыточно? (пардон, каламбурI8 */ 1955 - 1956 return FALSS; 1957 } Строки 1946-1957 аналогичны. Они устанавливают вывод родителя на потомка, сохраняя дескриптор файла для записывающего конца канала от родителя к потомку в FILE*, используя функцию fdopen (). Если это завершается неудачей, строки 1947— 1957 предпринимают те же действия, что и ранее: закрывают все дескрипторы каналов и посылают сигнал порожденным процессам. С этого момента записываемый конец канала от родителя к потомку и читаемый конец канала от потомка к родителю хранятся в более крупных структурах: FILE* и IOBUF соответственно. Они автоматически закрываются обычными процедурами, которые закрывают эти структуры. Однако, остаются две задачи: I960 1961 1962 1963 1964 1966 1967 os__close__on_exec (сtop [0] , os_close_on_exec (ptoc [1] , (void) close(ptoc[0]); (void) elose(ctop[l]); return TRUE; str, str, "pipe", "pipe", " from") " from") Игра слов: kill-overkill (избыточно - overkill). - Примеч. перев.
9.6. Рекомендуемая литература 329 1968 } 1977 } Строки 196Q-1961 устанавливают флаг close-on-exec для двух дескрипторов, которые остались открытыми. os_close_on_exec () является простой функцией-оболочкой, которая выполняет эту работу на Unix- и POSIX-совместимых системах, но ничего не делает на системах, в которых нет флага close-on-exec. Это скрывает проблему переносимости в одном месте и позволяет избежать в коде множества запутывающих #ifdef здесь и в других местах io . с. Наконец, строки 1963-1964 закрывают концы каналов, которые не нужны родителю, а строка 1967 возвращает TRUE для обозначения успеха. 9.6. Рекомендуемая литература Управление заданиями сложно, включает группы процессов, сеансы, механизмы ожидания, сигналы и манипулирование группой процессов терминала. По существу, мы решили не вдаваться в детали. Однако, вы можете захотеть взглянуть на следующие книги: 1. Advanced Programming in the UNIX Environment, 2nd edition, by W. Richard Stevens and Stephen Rago. Addison-Wesley, Reading Massachusetts, USA, 2004. ISBN: 0-201-43307-9. Эта книга и полна, и основательна^ охватывая элементарное и продвинутое программирование под Unix. Она превосходно освещает группы процессов, сеансы, управление заданиями и сигналы. 2. The Design and Implementation of the 4.4 BSD Operating System, by Marshall Kirk McKu- sick, Keith Bostic, Michael J. Karels, and John S. Quarterman. Addison-Wesley, Reading, Massachusetts, USA, 1996. ISBN: 0-201-54979-4. Эта книга дает хороший обзор того же материала, включая обсуждение структур данных ядра, которое можно найти в разделе 4.8 этой книги. 9.7. Резюме • Новые процессы создаются с помощью fork(). После этого оба процесса исполняют один и тот же код, причем единственным различием является возвращаемое значение: 0 в порожденном процессе и положительный номер PID в родительском. Порожденный процесс наследует копии почти всех атрибутов родителя, наиболее важными из которых являются, пожалуй, открытые файлы. Унаследованные разделяемые дескрипторы файлов делают возможным многое из высокоуровневой семантики Unix и элегантные управляющие структуры оболочки. Это одна из наиболее фундаментальных частей оригинального дизайна Unix. Из-за разделения дескрипторов файл на самом деле не закрывается до тех пор, пока не будет закрыт последний открытый дескриптор файла. Это в особенности касается каналов, но затрагивает также освобождение дисковых блоков для удаленных, но все еще открытых файлов.
330 Глава 9. Управление процессами и каналы Вызовы getpidO и getppidO возвращают ID текущего и родительского процессов соответственно. Родителем процесса, первоначальный родитель которого завершается, становится специальный процесс init с PID 1. Таким образом, PPID может меняться, и приложения должны быть готовы к этому. • Системный вызов nice () дает возможность настраивать приоритет вашего процесса. Чем приятнее вы по отношению к другим процессам, тем меньше ваш относительный приоритет, и наоборот. Лишь суперпользователь может иметь больший приоритет по сравнению с другими процессами. На современных системах, особенно однопользовательских, нет действительных причин для изменения значения относительного приоритета. Системный вызов exec () начинает исполнение новой программы в существующем процессе. Шесть различных версий вызова предоставляют гибкость в установке списков аргументов и окружения ценой первоначальной путаницы по поводу того, какую из них лучше всего использовать. Два варианта имитируют механизм поиска оболочки и отступают к использованию оболочки для интерпретации файла в случае, если он не является двоичным исполняемым файлом; эти варианты должны использоваться с предусмотрительностью. • Значение argv [ 0 ] для новой программы обычно происходит от имени исполняемого файла, но это лишь соглашение. Как и в случае с fork (), значительный, но не идентичный набор атрибутов наследуется через exec. Другие атрибуты сбрасываются для использования подходящих значений по умолчанию. • Функция at exit () регистрирует функции обратного вызова для вызова в порядке LIFO при завершении программы. Функции exit(), __exit() и _Exit() все завершают программу, передавая статус завершения обратно родителю, exit О очищает открытые потоки FILE* и запускает функции, зарегистрированные с помощью atexit (). Две другие функции завершаются немедленно и должны использоваться, лишь когда exec в порожденном процессе завершилась неудачей. Возвращение из main () подобно вызову exit () с данным возвращаемым значением. В С99 и C++ выпадение из main() в конце функции дает тот же результат, что и 'exit @) \ но является плохой практикой. • wait () и w&itpidO являются функциями POSIX для получения статуса завершения порожденного процесса. Различные макросы позволяют определить, завершился ли порожденный процесс нормально, и в таком случае определить статус его завершения, или же порожденный процесс претерпел сигнал завершения, и в этом случае определить совершивший этот проступок сигнал. Со специальными опциями waitpid() предоставляет также сведения о потомках, которые не завершились, но изменили состояние. • Системы GNU/Linux и большинство Unix-систем поддерживают также функции BSD wait3 () и wait4 (). GNU/Linux поддерживает также выходящий из употребления union wait. Функции BSD предоставляют struct rusage, давая доступ к сведениям об использовании времени процессора, что может быть удобным. Хотя если waitpid {) будет достаточной, то это наиболее переносимый способ выполнения. • Группы процессов являются частью более крупного механизма управления заданиями, который включает сигналы, сеансы и манипулирование состоянием терминала, getpgrp () возвращает ID группы процессов текущего процесса, a getpgid () возвращает PGID определенного процесса. Сходным образом, setpgrpO устанавливает PGID текущего процесса равным его PID, делая его лидером группы процессов;
Упражнения 331 setpgid () дает возможность родительскому процессу установить PGID порожденного, который еще не выполнил exec. • Каналы и FIFO предоставляют односторонний коммуникационный канал между двумя процессами. Каналы должны быть установлены общим предком, тогда как FIFO могут использоваться любыми двумя процессами. Каналы создаются с помощью pipe О, а файлы FIFO создаются с помощью mkfifoO. Каналы и FIFO буферируют свои данные, останавливая производителя или потребителя, когда канал заполняется или пустеет. dup () и dup2 () создают копии дескрипторов открытых файлов. В сочетании с closet) они дают возможность поместить дескрипторы файлов на место стандартного ввода и вывода для каналов. Чтобы каналы работали правильно, все копии неиспользуемых концов каналов до исполнения программой назначения exec должны быть закрыты. Для создания нелинейных каналов может быть использован /dev/ f d, что демонстрируется возможностью замещения процессов оболочками Bash и Когп. • fcntlO является функцией для выполнения различных работ. Она управляет атрибутами как самого дескриптора файла, так и лежащего в его основе файла. В данной главе мы видели, что f cntl () используется для следующего: • Дублирования дескриптора файла, имитирования dup () и почти имитирования dup2 (). • Получения и установки флага close-on-exec. Флаг close-on-exec является в настоящее время единственным атрибутом дескриптора файла, но он важен. Он не копируется в результате действия dup (), но должен явным образом устанавливаться для дескрипторов файлов, которые не должны оставаться открытыми после выполнения exec. На практике, это должно быть сделано для большинства дескрипторов файла. • Получение и установка флагов, управляющих нижележащим файлом. Из них 0_NONBLOCK является, пожалуй, наиболее полезным, по крайней мере, для FIFO и каналов. Это определенно самый сложный флаг. Упражнения 1. Напишите программу, которая выводит как можно больше сведений о текущем процессе: PID, PPID, открытые файлы, текущий каталог, значение относительного приоритета и т. д. Как вы можете сказать, какие файлы открыты? Если несколько дескрипторов файлов ссылаются на один и тот же файл, укажите это. (Опять-таки, как вы можете это узнать?) 2. Как вы думаете, atexit () хранит указатели на функции обратного вызова? Реализуйте atexit (), держа в уме принцип GNU «никаких произвольных ограничений». Набросайте схему (псевдокод) для exit (). Каких сведений (внутренностей библиотеки <stdio. h>) вам не хватает, чтобы написать exit () ? 3. Программа xargs предназначена для многократных запусков команды и аргументов, когда аргументов слишком много для непосредственного набора в командной строке. Программа работает, считывая строки из стандартного ввода, рассматривая каждую строку в качестве отдельного аргумента для указанной команды, и упаковывая аргументы до тех пор, пока они остаются в пределах максимально допустимого для системы. Например:
332 Глава 9. Управление процессами и каналы $ grep ARG_MAX /usr/Include/*.h /usr/include/*/*.h /* Командная строка .*/ bash: /bin/grep: Argument list too long /* Сообщение оболочки об ошибке */ $ find /usr/include -name • *.h' | xargs grep ABGJMAX /* find b xargs работают */ /usr/include/sys/param.h:#define NCARGS ARG_MAX Константа ARG__MAX в <limits .h> представляет сочетание общей памяти, используемой средой, и аргументов командной строки. Стандарт POSIX не говорит, включает ли это массивы указателей или просто сами строки. Напишите простую версию xargs, которая работает указанным способом. Не забудьте об окружении при вычислении размера необходимого пространства. Убедитесь, что тщательно управляете памятью. 4. Компоновка значения status, заполняемого функциями wait() и waitpidf), стандартом POSIX не определяется. Хотя и историческое, это 16-разрядное значение, которое выглядит, как показано на рис. 9.8. 15 14 13 12 11 10 9 8 7 б 5.4 3 2 1 0 Статус завершения 'или ' Г CD j ' Сигнал завершения ' ' I сигнал остановки II §§ ¦¦¦¦bin ¦¦!¦ ¦ i, nnili ' li mLm I fan m\ mil. ¦ t 4 *iL »ii J Рис. 9.8. Компоновка значения status функции wait() • Ненулевое значение в битах 0-7 указывает на завершение по сигналу. • Все единичные биты в поле сигнала указывает, что порожденный процесс остановлен. В этом случае биты 9-15 содержат номер сигнала. • Единичное значение бита 8 указывает завершение со снимком процесса. • Если биты 0-7 равны нулю, процесс завершился нормально. В этом случае биты 9-15 являются статусом завершения. Напишите с данными сведениями макросы POSIX WIFEXITED () и др. 5. Помня, что dup2 () сначала закрывает запрошенный дескриптор файла, реализуйте dup2 (), используя close () и f cntl (). Как вы обработаете случай, когда f cntl () возвращает значение меньше запрошенного? 6. Есть ли на вашей системе каталог /dev/ f d? Если есть, как он реализован? 7. Напишите новую версию chO9-pipeline.с, которая порождает лишь один процесс. После порождения родитель должен поменять дескрипторы своих файлов и сам выполнить exec для одноц из новых программ. 8. (Трудное) Как вы можете узнать, вызывал ли ваш процесс когда-нибудь chroot О ? Напишите программу, которая проверяет это и выводит сообщение с ответом да или нет. Можно ли обмануть вашу программу? Если да, как? 9. Есть ли на вашей системе каталог /ргос? Если да, доступ к какой информации о процессе он обеспечивает?
Глава 10 Сигналы В этой главе: • 10.1. Введение 333 • 10.2. Действия сигнала .... , 334 • 10.3. Стандартные сигналы С: signal () и raise () 335 • 10.4. Обработчики сигналов в действии 339 • 10.5. API сигналов System V Release 3: sigset () и др 350 • 10.6. Сигналы POSIX .... 351 • 10.7. Сигналы для межпроцессного взаимодействия 361 • 10.8. Важные сигналы специального назначения 364 • 10.9. Сигналы, передающиеся через fork () и exec (Ь 380 • 10.10. Резюме , 381 • Упражнения. 383 Данная глава освещает все подробности сигналов, важную, но сложную часть GNU/Linux API. 10.1. Введение Сигнал является указанием, что случилось какое-то событие, например, попытка сослаться на адрес памяти, который не является частью адресного пространства вашей программы, или когда пользователь нажимает CTRL-C для выхода из программы (называется генерированием прерывания): Программа может узнать лишь, что определенный сигнал был по крайней мере однажды. Обычно вы не можете сказать, случился ли один и тот же сигнал несколько раз. Вы можете отличить один сигнал от другого и управлять способом реагирования программы на различные сигналы. Механизмы обработки сигналов развились с течением времени. Как бывает почти со всеми такими механизмами, стандартизованы и доступны как первоначальные, так и более новые API. Однако, из фундаментальных API обработка сигналов обнаруживает, возможно, самые широкие изменения; имеется множество возможностей обработки, чтобы преуспеть в использовании наиболее подходящего API. В результате, возможно, это самая трудная глава в книге. Мы сделаем все возможное, чтобы сделать изложение более ясным, но если вы проработаете эту главу более тщательно, чем обычно, это поможет.
334 Глава 10. Сигналы В отличие от большинства глав в данной книге, наше представление здесь историческое, связанное с освещением развития API, включая API, которые никогда не следует использовать в новом коде. Мы делаем это, потому что это упрощает изложение, делая понятным, почему функция POSIX API sigactionO поддерживает все те возможности, которые поддерживает. 10.2. Действия сигналов Каждый сигнал (вскоре мы представим полный список) имеет связанное с ним действие по умолчанию. POSIX обозначает это как диспозицию (disposition) сигнала. Это то действие, которое ядро осуществляет для процесса, когда поступает определенный сигнал. Действие по умолчанию варьирует: Завершение Процесс завершается. Игнорирование Сигнал игнорируется. Программа никогда не узнает, что что-то случилось. Снимок образа процесса Процесс завершается, и ядро создает файл core (в текущем каталоге процесса), содержащий образ работавшей на момент поступления сигнала программы. Снимок процесса может впоследствии использоваться с отладчиком для исследования состояния программы (см. главу 15 «Отладка»). По умолчанию системы GNU/Linux создают файлы с именем core. pi d, где pi d является ID завершаемого процесса. (Это можно изменить; см. sysctl(%).) Такое именование позволяет хранить в одном и том же каталоге несколько файлов core, за счет использования большего дискового пространства1. Традиционные системы Unix называют файл core, и это ваше дело сохранить какие-нибудь файлы core для последующего изучения, если есть шанс создания других таких же файлов в том же каталоге. Остановка Процесс останавливается. Впоследствии он может быть возобновлен. (Если вы использовали управление заданиями оболочки с помощью CTRL-Z, f g и bg, вы понимаете остановку процесса.) По крайней мере один поставщик дистрибутивов GNU/Linux отменяет создание файлов core «с иголочки». Для повторного подключения этой возможности поместите в свой файл -/.profile строку 'ulimit -S -с unlimited'. - Примеч. автора.
10.3. Стандартные сигналы С: signal () и raise () 335 10.3. Стандартные сигналы С: signal () И raise() Стандарт ISO С определяет первоначальный API управления сигналами V7 и новый API для посылки сигналов. Вы должны использовать их для программ, которым придется работать на не-POSIX системах, или в случаях, когда предоставляемые ISO С API возможности являются достаточными. 10.3.1. Функция signal () Действие сигнала изменяется с помощью функции signal (). Вы можете изменить действие на «игнорировать сигнал», «восстановить для сигнала действие системы по умолчанию» или «вызвать при появлении сигнала мою функцию с номером сигнала в качестве параметра». Функция, которую вы предоставляете для распоряжения сигналом, называется обработчиком сигнала (или просто обработчиком), а установка ее в соответствующем месте осуществляет перехват (catch) сигнала. Получив эти сведения, давайте перейдем к API. В заголовочном файле <signal ,h> представлены определения макросов для поддерживаемых сигналов и объявления функций управления сигналами, предоставляемыми стандартом С: #include <signal.h> /* ISO С */ void (*signal(int signum, void (*func)(int)))(int); Это объявление для функции signal () почти невозможно прочесть. Поэтому справочная страница GNU/Linux signal{2) определяет ее таким способом: typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); Теперь это более вразумительно. Тип sighandler_t является указателем на функцию с возвращаемым типом void, которая принимает один целый аргумент. Это целое является номером поступающего сигнала. Функция signal () принимает номер сигнала в качестве своего первого параметра, а указатель функции (новый обработчик) в качестве своего второго аргумента. Если последний не является указателем функции, он может быть лишь SIG_DEF, что означает «восстановить действие по умолчанию», либо SIG_IGN, что означает «игнорировать сигнал». signal () изменяет действие для signum и возвращает предыдущее действие. (Это дает вам возможность восстановить при желании предыдущее действие.) Возвращаемое значение может равняться также SIG_ERR, что указывает на произошедшую ошибку. (Некоторые сигналы невозможно перехватить или игнорировать; предоставление для них обработчика сигнала или неверный signum создают эту ошибку.) В табл. 10.1 перечислены сигналы, доступные под GNU/Linux, их числовые значения, действия по умолчанию
336 Глава 10. Сигналы для каждого, формальный стандарт или современная операционная система, которые их определяют, и смысл каждого. Более старые версии оболочки Борна (/bin/sh) непосредственно связывали с номерами сигналов ловушки (traps), которые являются обработчиками сигналов на уровне оболочки. Таким образом, всесторонне образованному Unix-программисту нужно было знать не только имена сигналов для использования в коде С, но также и соответствующие номера сигналов! POSIX требует, чтобы команда trap понимала символические имена сигналов (без префикса 'SIG'), поэтому этого больше не требуется. Однако (главным образом для лучшего разбирательства), мы предоставили эти номера в интересах полноты из-за того, что однажды вам может понадобиться иметь дело со сценарием оболочки, созданным до POSIX, или с древним кодом на С, которые непосредственно используют номера сигналов. ХЗЛМЕЧАНИЕ. Для некоторых более новых сигналов, от 16 и выше, соответствующие номера сигнала и их имена на различных платформах не обязательно совпа- Хдают! Проверьте заголовочные файлы и справочные страницы на своей системе. I Табл. 10.1 верна для GNU/Linux. Таблица 10.1. Сигналы GNU/Linux Имя SIGHUP SIGINT SIGQUIT SIGILL SIGTRAP SIGABRT SIGIOT SIGBUS SIGFPE SIGKILL SIGUSR1 SIGSEGV SIGUSR2 SIGPIPE SIGALRM SIGTERM Значение 1 2 3 4 5 6 6 7 8 9 10 11 12 13 14 15 По умолчанию Term Term Core Core Core Core Core Core Core Term Term Core Term Term Term Term Источник POSIX ISO С POSIX ISO С POSIX ISO С BSD BSD ISO С POSIX POSIX ISO С POSIX POSIX POSIX ISO С Смысл Отсоединение Прерывание Выход Недействительная инструкция Трассировочная ловушка Прекращение Ловушка ЮТ Ошибка шины Исключение с плавающей точкой Завершение, неблокируемый Сигнал 1 пользователя Нарушение сегмента Сигнал 2 пользователя Нарушенный канал Аварийные часы Завершение
10.3. Стандартные сигналы С: signal () и raise () Таблица 1.0.1. Сигналы GNU/Linux (Продолжение) 337 Имя Значение По умолчанию Источник Смысл SIGSTKFLT SIGCHLD SIGCLD SIGCONT SIGSTOP SIGTSTP SIGTTIN SIGTTOU SIGURG SIGXCPU SIGXFSZ SIGVTALRM SIGPROF SIGWINCH SIGIO SIGPOLL SIGPWR SIGSYS Обозначения: 16 17 17 18 19 20 21 22 23 24 25 26 27 28 29 29 30 31 Core: Ignr: Stop: Term Term Ignr Ignr Stop Stop Stop Stop Ignr Core Core Term Term Ignr Term Term Term Core Linux POSIX System V POSIX POSIX POSIX POSIX POSIX BSD BSD BSD BSD BSD BSD BSD System V System V POSIX Ошибка стека в процессоре (не используется) Изменение статуса порожденного процесса То же, что и SIGCHLD (для совместимости) Продолжить при остановке Стоп, неблокируемый Стоп от клавиатуры Фоновое чтение от tty Фоновая запись в try Срочный сигнал сокета Превышение предела процессора Превышение предела размера файла Виртуальные аварийные часы Профилирующие аварийные часы Изменение размера окна Возможен ввод/вывод Опрашиваемое событие: то же, что и SIGIO (для совместимости) Повторный запуск из-за сбоя питания Неверный системный вызов Завершить процесс и создать снимок образа процесса. Игнорировать сигнал. Остановить процесс. Завершить процесс. Некоторые системы определяют также и другие сигналы, такие, как SIGEMT, SIGLOST и SIGINFO. Справочная страница GNU/Linux signal(l) предоставляет полный список; если ваша программа должна обработать сигналы, не поддерживаемые GNU/Linux, это можно сделать с помощью #if def:
338 Глава 10. Сигналы #ifdef SIGLOST /* ...обработать здесь SIGLOST... */ #endif За исключением SIGSTKFLT, сигналы, перечисленные в табл. 10.1, широко доступны и не нуждаются в заключении в #if def. Сигналы SIGKILL и SIGSTOP нельзя перехватить или игнорировать (или блокировать, как описано далее в главе). Они всегда выполняют действие по умолчанию, указанное в табл. 10.1. Чтобы увидеть список поддерживаемых сигналов, вы можете использовать 'kill -1\ На одной из наших систем GNU/Linux: $ kill -l 1) 5) 9) 13) 18) 22) 26) 30) 34) 38) 42) 46) 50) 54) 58) 62) SIGHUP SIGTRAP SIGKILL SIGPIPE SIGCONT SIGTTOU SIGVTALRM SIGPWR SIGRTMIN+2 SIGRTMIN+6 SIGRTMIN+10 SIGRTMIN+14 SIGRTMAX-13 SIGRTMAX-9 SIGRTMAX-5 SIGRTMAX-1 2) 6) 10) 14) 19) 23) 27) 31) 35) 39) 43) 47) 51) 55) 59) 63) SIGINT SIGABRT SIGUSR1 SIGALRM SIGSTOP SIGURG SIGPROF SIGSYS SIGRTMIN+3 SIGRTMIN+7 SIGRTMIN+11 SIGRTMIN+15 SIGRTMAX-12 SIGRTMAX-8 SIGRTMAX-4 SIGRTMAX 3) 7) 11) 15) 20) 24) 28) 32) 36) 40) 44) 48) 52) 56) 60) SIGQUIT SIGBUS SIGSEGV SIGTERM SIGTSTP SIGXCPU SIGWINCH SIGRTMIN SIGRTMIN+4 SIGRTMIN+8 SIGRTMIN+12 SIGRTMAX- SIGRTMAX- SIGRTMAX- SIGRTMAX- -15 -11 -7 -3 4) 8) 12) 17) 21) 25) 29) 33) 37) 41) 45) 49) 53) 57) 61) SIGILL SIGFPE SIGUSR2 SIGCHLD SIGTTIN SIGXFSZ SIGIO SIGRTMIN+1 SIGRTMIN+5 SIGRTMIN+9 SIGRTMIN+13 SIGRTMAX-14 SIGRTMAX-10 SIGRTMAX-6 SIGRTMAX-2 Сигналы SIGRTXXX являются сигналами реального времени, сложная тема, которую мы не будем рассматривать. 10.3.2. Программная отправка сигналов: raise () Помимо внешнего генерирования, сигнал может быть отправлен непосредственно самой программой с использованием стандартной функции С raise (): #include <signal.h> !/* ISO С */ int raise(int sig); Эта функция посылает сигнал sig вызывающему процессу. (Это действие имеет свое применение; вскоре мы увидим пример.) Поскольку raise () определена стандартом С, для процесса это наиболее переносимый способ отправить себе сигнал. Есть другие способы, которые мы обсудим далее в главе.
10.4. Обработчики сигналов в действии 339 10.4. Обработчики сигналов в действии Множество осложнений и отклонений обнаруживается после установки на место обработчика, после его вызова и впоследствии возвращения. 10.4.1. Традиционные системы После помещения на место обработчика сигнала ваша программа развивается своим путем. Интересные вещи возникают лишь с появлением сигнала (например, пользователь нажал CTRL-C для прерывания вашей программы, или был сделан вызов raise ()). По получении сигнала ядро останавливает процесс, где бы он ни был. Затем оно имитирует вызов процедуры обработчика сигнала, передавая ему номер сигнала в качестве ее единственного аргумента. Ядро устраивает все таким образом, что нормальный возврат из функции обработчика сигнала (либо посредством return, либо в результате выпадения из конца функции) передает управление в ту точку программы, в которой она находилась в момент появления сигнала. Что происходит после обработки сигнала, когда тот же самый сигнал появится в следующий раз снова? Остается ли обработчик на том же месте? Или же он сбрасывается, и для сигнала используется действие по умолчанию? Ответ, по историческим причинам, «зависит от». В частности, стандарт С оставляет это на усмотрение реализации. На практике V7 и традиционные системы System V, такие, как Solaris, устанавливают для сигнала действие по умолчанию. Давайте рассмотрим простой обработчик сигнала в действии под Solaris. Следующая программа, chlO-catchint .с, перехватывает SIGINT. Обычно вы генерируете этот сигнал, набирая на клавиатуре CTRL-C. 1 /* chlO-catchint.с - перехват SIGINT, по крайней мере, однажды. */ 2 < 3 #include <signal.h> 4 #include <string.h> 5 #include <unistd.h> 6 ¦.'.¦'¦'¦..'.¦.¦ 7 /* handler простой обработчик сигнала. */ 8 9 void handler(int signum) j 10 { 11 char buf[200], *cp; 12 int offset; 13 14 /* Пройти через это испытание , чтобы избежать fprintf(). */ 15 strcpy(buf, "handler:, caught signal ") ; 16 cp = buf + strlen(buf); /* cp указывает на завершающий '\0' */ 17 if (signum > 100) /* маловероятно */
340 Глава 10. Сигналы 18 offset = 3; 19 else if (signum > 10) 20 offset = 2; 21 else 22 offset = 1; 23 cp += offset; 24 • ; 25 *cp-- = '\0'; /* завершить строку */ 26 while (signum > 0) { /* work backwards, filling in digits */ 27 *cp-- = (signum % 10) + ' 0'; 28 signum /= 10; 29 } 30 strcat(buf, "\n"); 31 (void)* write B, buf, strlen(buf) ) ; 32 } 33 34 /* main установить обработку сигнала и войти в бесконечный цикл */ 35 36 int main(void) 37 { 38 (void) signal(SIGINT, handler); 39 40 for (;;) 41 pause () ; /* ждать сигнал, см. далее в главе */ 42 43 return 0; 44 } Строки 9-32 определяют функцию обработки сигнала (остроумно названную handler (J). Все, что эта функция делает, - выводит номер перехваченного сигнала и возвращается. Для вывода этого сообщения она выполняет множество ручной работы, поскольку f print f () не яв- , ляется «безопасной» для вызова из обработчика сигнала. (Вскоре это будет описано в разделе 10.4.6 «Дополнительные предостережения».) Функция main () устанавливает обработчик сигнала (строка 38), а затем входит в бесконечный цикл (строки 40-41). Вот что происходит при запуске: $ ssh solar is. example, cam /* Зарегистрироваться на доступной системе Solaris */ Last login: Fri Sep 19 04:33:25 2003 from 4.3.2.1. Sun Microsystems Inc. SunOS 5.9 Generic May 2002 $ gcc chlO-catchint.c '/* Откомпилировать программу */ $ a.out /* Запустить ее */ AChandler: caught signal 2 /* Набрать АС, вызывается обработчик */ AC /* Попробовать снова, но на этот раз... */ $ ¦ /* Программа завершается */ Handler (англ.) - обработчик. - Примеч. перев.
10.4. Обработчики сигналов в действии 341 Поскольку V7 и другие традиционные системы восстанавливают действие сигнала по умолчанию, поэтому когда вы хотите снова получить сигнал в будущем, функция обработчика должна немедленно переустановить саму себя: , void handler(int signum) { char buf[200], *cp; int offset; (void) signal(signum, handler); /* переустановить обработчик */ /* ...оставшаяся часть функции как прежде... */ } 10.4.2. BSD и GNU/Linux BSD 4.2 изменила способ работы signal () 3. На системах BSD обработчик сигнала после его возвращения остается на месте. Системы GNU/Linux следуют поведению BSD. Вот что происходит под GNU/Linux: $ chlO-catchint /* Запустить программу */ handler: caught signal 2/* Набираем ЛС, вызывается обработчик */ handler: caught signal 2 /* И снова... */ handler: caught signal 2 /* И снова! */. handler: caught signal 2 /* Помогите! */ handler: caught signal 2 /* Как нам это остановить?! */ Quit (core dumped) /* Л\, генерирует SIGQUIT. Bay */ На системе BSD или GNU/Linux обработчик сигнала не должен дополнительно использовать 'signal (signum, handler)' для переустановки обработчика. Однако, лишний вызов не причиняет никакого вреда, поэтому сохраняется статус кво. В действительности, POSIX предоставляет функцию bsd_signal (), которая идентична signal () за тем исключением, что она гарантирует, что обработчик сигнала останется установленным: #include <signal.h> /* XSI, устаревает */ void (*bsd_signal(int sig, void (*func)(int)))(int); Это устраняет проблемы переносимости. Если вы знаете, что ваша программа будет работать лишь на системах POSIX, вы можете воспользоваться bsd_s ignal () вместо s ignal (). Одно предостережение - эта функция также помечена как «устаревающая», что означает возможность отказа от нее в будущем стандарте. На практике, даже если от нее откажутся, поставщики скорее всего долгое время будут ее поддерживать. (Как мы увидим, функция API POSIX sigaction() предоставляет достаточно возможностей для написания рабочей версии, если это вам нужно.) Изменение поведения было плохой мыслью, сильно критиковавшейся в свое время, но было слишком поздно. Изменение семантики определенного интерфейса всегда ведет к проблеме, как было в этом случае. Хотя это особенно относится к проектировщикам операционных систем, любой, кто разрабатывает библиотеки общего назначения, также должен помнить этот урок. - Примеч. автора.
342 Глава 10. Сигналы 10.4.3. Игнорирование сигналов Более практично, когда вызывается обработчик сигнала, это означает, что программа должна завершиться и выйти. Было бы раздражающим, если бы большинство программ по получении SIGINT выводили бы сообщение и продолжали работу; смысл сигнала в том, что они должны остановиться! . . . Например, рассмотрите программу sort, sort, возможно, создала любое число временных файлов для использования на промежуточных этапах процесса сортировки. По получении SIGINT, sort должна удалить временные файлы и выйти. Вот упрощенная версия обработчика сигнала из GNU Coreutils sort. с: /* Обработка прерываний и отсоединений. Упрощена для представления */ static void sighandler (int sig) { signal (sig, SIG_IGN); /* Отныне этот сигнал игнорировать */ cleanup (); /* Очистка после себя */ signal (sig, SIG_DFL); /* Восстановление действия по умолчанию */ raise (sig); /* Повторно отправить сигнал */ } Установка действия SIG_IGN гарантирует, что все последующие появляющиеся сигналы SIGINT не повлияют на продолжающийся процесс очистки. Когда функция cleanup () завершит работу, восстановление действия SIG_DFL позволяет системе сделать снимок образа процесса, если это нужно возникшему сигналу. Вызов raise () восстанавливает сигнал. Затем восстановленный сигнал вызывает действие по умолчанию, которое, скорее всего, завершит программу. (Далее в этой главе мы полностью покажем обработчик сигнала sort. с.) 10.4.4. Системные вызовы, допускающие повторный запуск Значение EINTR для errno (см. раздел 4.3 «Определение ошибок») указывает, что системный вызов был прерван. Хотя с этим значением ошибки может завершаться большое количество системных вызовов, двумя наиболее значительными являются read () и write (). Рассмотрите следующий код: void handler(int signal) { /* обработка сигналов */ } int main(int argc, char **argV) { signal(SIGINT, handler); while ((count = read(fd, buf, sizeof buf)) > 0) { /* Обработка буфера */ } if (count '== 0) * ' •
10.4. Обработчики сигналов в действии 343 /* конец файла, очистка и т.п. */ else if (count == -1) /* ошибка */ > " Предположим, что система успешно прочла (и заполнила) часть буфера, когда возник SIGINT. Системный вызов read () еще не вернулся из ядра в программу, но ядро решает, что оно может доставить сигнал. Вызывается handler (), запускается и возвращается в середину read (). Что возвратит read ()¦? В былые времена (V7, более ранние системы System V) read() возвратила бы -1 и установила бы errno равным EINTR. Не было способа сообщить, что данные были переданы. В данном случае V7 и System V действуют, как если бы ничего не случилось: не было перемещений данных в и из буфера пользователя, и смещение файла не было изменено, BSD 4.2 изменила это. Были два случая: Медленные устройства «Медленное устройство» является в сущности терминалом или почти всяким другим устройством, кроме обычного файла. В этом случае read () могла завершиться с ошибкой EINTR, лишь если не было передано никаких данных, когда появился сигнал. В противном случае системный вызов был бы запущен повторно, и read () возвратилась бы нормально. Обычные файлы Системный вызов был бы запущен повторно. В этом случае read () вернулась бы нормально; возвращенное значение могло быть либо числом запрошенных байтов, либо числом действительно прочитанных байтов (как в случае чтения вблизи конца файла). Поведение BSD несомненно полезно; вы всегда можете сказать, сколько данных было прочитано. Поведение POSIX сходно, но не идентично первоначальному поведению BSD. POSIX указывает, что read (L завершается с ошибкой EINTR лишь в случае появления сигнала до начала перемещения данных. Хотя POSIX ничего не говорит о «медленных устройствах», на практике это условие проявляется именно на них. В противном случае, если сигнал прерывает частично выполненную read (), возвращенное значение является числом уже прочитанных байтов. По этой причине (а также для возможности обработки коротких файлов) всегда следует проверять возвращаемое read () значение и никогда не предполагать, что прочитано все запрошенное количество байтов. (Функция POSIX API sigaction (), описанная позже, позволяет при желании получить поведение повторно вызываемых системных вызовов BSD.) Хотя мы описываем readO, эти правила применяются ко всем системным вызовам, которые могут завершиться с ошибкой EINTR, как, например, семейство функций wait () .-Примеч. автора.
344 Глава 10. Сигналы 10.4.4.1. Пример: GNUCoreutils safe^read () иsafe_wri te() Для обработки случая EINTR в традиционных системах GNU Coreutils использует две функции, saf e__read () и saf e_write (). Код несколько запутан из-за того, что один и тот же файл за счет включения #include и макросов реализует обе функции. Из файла lib/saf е-read. с в дистрибутиве Coreutils; 1 /* Интерфейс read и write для ловторных запусков после прерываний. 2 Copyright (С) 1993, 1994, 1998, 2002 Free Software Foundation, Inc. /* ...куча шаблонного материала опущена... */ 56 57 #ifdef SAFE_WRITE 58 # include "safe-write.h" 59 # define safe__rw safe_write /* Создание safe_write() */ 60 # define rw write /* Использование системного вызова write() */ 61 #else 62 # include "safe-read.h" 63 # define safe__rw safe__read /* Создание safe_read() */ 64 # define rw read /* Использование системного вызова read О */ 65 # undef const 66 # define const /* пусто */ 67 #endif 68 69 /* Прочесть (записать) вплоть до COUNT байтов в BUF из (в) дескриптора FD, повторно запуская вызов при 70 прерывании. Вернуть число действительно прочитанных (записанных) байтов, 0 для ЮР 71 или в случае ошибки SAFE_READ_ERROR(SAFE_WRITE_ERROR). */ 72 size_t 73 safe_rw (int fd, void const *buf, size_t count) 74 { 75 ssize_t result; 76 77 /* POSIX ограничивает ООШГГ значением SSIZEjyiAX, но мы еще больше ограничиваем его, требуя, 78 чтобы COUNT <= INT_MAX, для избежания ошибки в Tru64 5.1. 79 При уменьшении COUNT сохраняйте указатель файла выровненным по размеру блока. 80 Офатите внимание, что read (write) может быть успешным в любом случае, даже если прочитано (записано) 81 менее COUNT байтов, поэтому вызывающий должен быть готов обработать 82 частичные результаты. */ 83 if (count > ШТ_МАХ) 84 count = INT_MAX & -8191; 86 do 87 { 88 result = rw (fd, buf, count); 89 } 90 while (result < 0 && IS_EINTR (errno));
10.4. Обработчики сигналов в действии 345 91 92 return (size_t) result; 93 } Строки 57-67 обрабатывают определения, создавая соответствующим образом safe_read() и safe_write () (см. ниже safe_write .с). Строки 77-84 указывают на разновидность осложнений, возникающих при чтении. Здесь один особый вариант Unix не может обработать значения, превышающие INT_MAX, поэтому строки 83-84 выполняют сразу две операции: уменьшают значение числа, чтобы оно не превышало INT__MAX, и сохраняют его кратным 8192. Последняя операция служит эффективности дисковых операций: выполнение ввода/вывода с кратным основному размеру дискового блока объемом данных более эффективно, чем со случайными размерами данных. Как отмечено в комментарии, код сохраняет семантику read () и write (), где возвращенное число байтов может быть меньше затребованного. Обратите внимание, что параметр count может и в самом деле быть больше INT_MAX, поскольку count представляет TOn.size_t, который является беззнаковым (unsigned). INT__MAX является чистым int, который на всех современных системах является знаковым. Строки 86-90 представляют действительный цикл, повторно осуществляющий операцию, пока она завершается ошибкой EINTR. Макрос IS_EINTR() не показан, но он обрабатывает случай в системах, на которых EINTR не определен. (Должен быть по крайней мере один такой случай, иначе код не будет возиться с установкой макроса; возможно, это было сделано для эмуляции Unix или POSIX в не-Unix системе.) Вот saf e_write. с: 1 /* Интерфейс write для повторного запуска после прерываний. 2 Copyright (С) 2002 Free Software Foundation, Inc. /* ... куча шаблонного материала опущена... */. 17 ' 18 #define SAFE__WRITE 19 #include "safe-read.с" В строке 18 #define определяет SAFE_WRITE; это связано со строками 57-60 в safe_read.c. 10.4.4.2. Только GLIB& temp__failure_retry() Файл <unis td. h> GLIBC определяет" макрос TEMP_FAILURE_RETRY (), который вы можете использовать для инкапсулирования любого системного вызова, который может при неудачном вызове установить errno в EINTR. Его «объявление» следующее: #include <unistd.h> /* GLIBC */ long int TEMP_FAILURE_RETRY(expression) ; Вот определение макроса: /* Оценить EXPRESSION и повторять, пока оно возвращает -1 с хerrno', установленным в EINTR. */ # define TEMP_FAILURE_RETRY(expression) \
346 Глава 10. Сигналы ( extension \ ({ long int result; \ do result = (long int) (expression); \ while ( result == -1L && errno == EINTR); \ result; })) Макрос использует расширение GCC к языку С (как обозначено ключевым словом extension ), которое допускает заключенным в фигурные скобки внутри обычных скобок выражениям возвращать значение, действуя таким образом подобно простому выражению. Используя этот макрос, мы могли бы переписать saf e_read () следующим образом: size_t safe_read(int fd, void const *buf, size_t count) { ssize_t result; /* Ограничить count, как в ранее приведенном комментарии. */ if (count > INT_MAX) count = INTJYIAX & -8191; result = TEMP_FAILURE_RETRY(read(fd, buf, count)); return (size_t) result; } 10.4.5. Состояния гонок и sig_atomic_t (ISO С) Пока обработка одного сигнала за раз выглядит просто: установка обработчика сигнала в main () и (не обязательная) переустановка самого себя обработчиком сигнала (или установка действия SIG_IGN) в качестве первого действия обработчика. Но что произойдет, если возникнут два идентичных сигнала, один за другим? В частности, что, если ваша система восстановит действие по умолчанию для вашего сигнала, а второй сигнал появится после вызова обработчика, но до того, как он себя восстановит? Или предположим, что вы используете bsd_signal (), так что обработчик остается установленным, но второй сигнал отличается от первого? Обычно обработчику первого сигнала нужно завершить свою работу до того, как запускается второй, а каждый обработчик сигнала не должен временно игнорировать все прочие возможные сигналы! Оба случая относятся к состоянию гонки. Одним решением для этих проблем является как можно большее упрощение обработчиков сигналов. Это можно сделать, создав флаговые переменные, указывающие на появление сигнала. Обработчик сигнала устанавливает переменную в true и возвращается. Затем основная логика проверяет флаговую переменную в стратегических местах: int sig__int_flag = 0; /* обработчик сигнала устанавливает в true */ void int_handler(int signum) { sig_int__f lag = 1; >
10.4. Обработчики сигналов в действии 347 int main(int argc, char **argv) { bsd_signal(SIGINT, int_handler); /* ...программа продолжается... */ if (sig_int__f lag) { /* возник SIGINT, обработать его */ } /*. ...оставшаяся логики... */ } (Обратите внимание, что эта стратегия уменьшает окно уязвимости, но не устраняет его.) Стандарт С вводит специальный тип - sig_atomic_t - для использования с такими флаговыми переменными. Идея, скрывающаяся за этим именем, в том, что присвоение значений переменным этого типа является атомарной операцией: т. е. они совершаются как одно делимое действие. Например, на большинстве машин присвоение значения int осуществляется атомарно, тогда как инициализация значений в структуре осуществляется либо путем копирования всех байтов в (сгенерированном компилятором) цикле, либо с помощью инструкции «блочного копирования», которая может быть прервана. Поскольку присвоение значения sig_atomic_t является атомарным, раз начавшись, оно завершается до того, как может появиться другой сигнал и прервать его. Наличие особого типа является лишь частью истории. Переменные sig_atomic_t должны быть также объявлены как volatile: volatile sig_atomic_t sig_int_flag = 0; /* обработчик сигнала устанавливает в true */ /*• ...оставшаяся часть кода как раньше... */ Ключевое слово volatile сообщает компилятору, что переменная может быть изменена извне, за спиной компилятора, так сказать. Это не позволяет компилятору применить оптимизацию, которая могла бы в противном случае повлиять на правильность кода. Структурирование приложения исключительно вокруг переменных sig_atomic_t ненадежно. Правильный способ обращения с сигналами показан далее, в разделе 10.7 «Сигналы для межпроцессного взаимодействия». 10.4.6. Дополнительные предостережения Стандарт POSIX предусматривает для обработчиков сигналов несколько предостережений: • Что случается, когда возвращаются обработчики для SIGFPE, SIGILL, SIGSEGV или любых других сигналов представляющих «вычислительные исключения», не определено. Если обработчик был вызван в результате вызова abort (), raise ( ) или kill (), он не может вызвать raise (). abort () описана в разделе 12.4 «Совершение самоубийства: abort () », a kill () описана дале$ в этой главе. (Описанная далее функция API sigatction () с обработчиком сигнала, принимающая три аргумента, дает возможность сообщить об этом, если это имеет место.) • Обработчики сигналов могут вызвать лишь функции из табл. 10.2. В частности, они должны избегать функций <stdio. h>. Проблема в том, что во время работы функции <stdio. h> может возникнуть прерывание, когда внутреннее состояние библио-
348 Глава 10. Сигналы течной функции находится в середине процесса обновления. Дальнейшие вызовы функций <stdio. h> могут повредить это внутреннее состояние. Список в табл. 10.2 происходит из раздела 2.4 тома System Interfaces (Системные интерфейсы) стандарта POSIX 2001. Многие из этих функций относятся к сложному API и больше не рассматриваются в данной книге. 10.4.7. Наша история до настоящего времени, эпизод 1 Сигналы являются сложной темой, и она становится еще более сбивающей с толку. Поэтому давайте на время сделаем остановку, сделаем шаг назад и подведем итог обсужденному до сих пор: • Сигналы являются указанием того, что произошло некоторое внешнее событие. raise (.) является функцией ISO С для отправки сигнала текущему процессу. Как отправлять сигналы другим процессам, нам еще предстоит описать. • signal () контролирует диспозицию сигнала, т. е. реакцию процесса на сигнал, когда он появляется. Сигнал можно оставить системе для обработки по умолчанию, проигнорировать или перехватить. • Когда сигнал перехватывается, вызывается функция-обработчик. Вот где сложность начинает поднимать свою безобразную голову: • ISO С не определяет, восстанавливается ли диспозиция сигнала по умолчанию до вызова обработчика или она остается на месте. Первое является поведением V7 и современных систем System V, таких, как Solaris. Последнее является поведением BSD, используемым также в GNU/Linux. (Для форсирования поведения BSD может использоваться функция POSIX bsd_signal ().) Таблица 10.2. Функции, которые могут быть вызваны из обработчика сигнала JExitO __exit () accept() access() aio_error() aio_return() aio_suspend() alarm() bind (J cfgetispeed() cfgetospeed() cfsetispeed() cfsetospeed() fpathconf() fstat() fsync() ftruncateO getegidO geteuidO getgidO getgroups() getpeername() getpgrp() getpidf) getppidf) getsockname() raise() read() readlink() recv() recvfrom() recvmsg() rename() rmdir() select() sem_post() send() sendmsg() sendto() sigqueue() sigset() sigsuspend() sleep() socket() socketpair() stat() sysmlink() sysconf() tcdrain() tcflowO tcflushO tcgetattr()
10.4. Обработчики сигналов в действии 349 Таблица 10.2. Функции, которые могут быть вызваны из обработчика сигнала (Продолжение) chdirO chmod() chown() clock_gettime() close() connect() creat() dup() dup2() execle() execve() fchmod() fchown() ¦fcntK) f4atasync() fork() getsockopt() getuidO killO link() listen() lseek() lstat() mkdir() mkfifoO open() pathconf() pause() pipe() pollO posix_trace_event () pselect() setgid() setpgid() setsidf) setsockopt() setuid() shutdown() sigaction() sigaddset() sigdelset() sigemptyset() sigfillset(() sigismember() signal() sigpauseO sigpending() sigprocmask() tcgetpgrpO tcsendbreak() tcsetattrf) tcsetpgrp() t ime() timer_jgetoverrun () timer_gettime() timer__settime () times() umask() , uname() unlink() utime() wait() waitpid() write() To, что случается при прерывании сигналом системного вызова, также различается в традиционной и BSD линейках. Традиционные системы возвращают -1 с errno, установленным в EINTR. BSD системы Повторно запускают системный вызов после возвращения из обработчика. Макрос GLIBC TEMP_FAILURE_RETRY () может помочь вам написать код для обработки системных вызовов, возвращающих -1 с errno, установленным в EINTR. POSIX требует, чтобы частично выполненный системный вызов возвращал успешное завершение, указав, сколько работы было выполнено. Системный вызов, который еще не начал выполняться, вызывается повторно. • Механизм signal () предоставляет плодотворную почву для появления условий гонки. В этой ситуации помогает тип данных ISO С sig_atomic__t, но он не решает проблему, и определенный таким способом механизм не может обезопасить от проявления условий гонки. • Применяется ряд дополнительных предостережений, и в частности, из обработчика сигнала безопасно может вызываться лишь часть стандартных библиотечных функций. Несмотря на эти проблемы интерфейса signal () для простых программ достаточно, и он все еще широко используется.
350 Глава 10. Сигналы 10.5. API сигналов System V Release 3: sigset () и др. BSD 4.0 (примерно с 1980 г.) ввел дополнительные функции API для предоставления «надежных» сигналов5. В частности, стало возможным блокировать сигналы. Другими словами, программа могла сообщить ядру: «Зависни на этих конкретных сигналах в течении следующего небольшого промежутка времени, затем доставь их мне, когда я буду готов их принять». Большим преимуществом является то, что эта особенность упрощает обработчики сигналов, которые автоматически запускаются со своим заблокированным сигналом (чтобыизбежать проблемы одновременной обработки двух сигналов) и, возможно, также и с другими заблокированными сигналами. System V Release 3 (примерно с 1984 г.) приняла эти API и популяризовала их; в большинстве связанных с Unix документации и книгах вы, возможно, увидите, что на эти API ссылаются, как ведущие начало от System V Release 3. Эти функции следующие: #include <signal.h> /* XSI */ int sighold(int sig); /* Добавить sig к маске сигналов процесса */ int sigrelse(int sig); /* Удалить sig из маски сигналов процесса */ int sigignore(int sig); /* Сокращение для sigset(sig, . SIG_IGN) */ int sigpause(int sig); /* Приостановить процесс, позволить появиться sig */ void (*sigset(int sig, void (*disp) (int))) (int); /* sighandler_t sigset (int sig, sighandler_t disp); */ Стандарт POSIX для этих функций описывает их поведение в терминах маски сигналов процесса. Маска сигналов процесса отслеживает, какие сигналы (если они вообще есть) процесс заблокировал в настоящее время. Более подробно это описывается в разделе 10.6.2 «Наборы сигналов: sigset_t и связанные функции». В API System V Release 3 нбт способа получить или изменить маску сигналов процесса в целом. Функции работают следующим образом: int sighold(int sig) , Добавляет sig к списку заблокированных процессов (маска сигналов процесса). int sigrelse(int sig) Удаляет sig из маски сигналов процесса. int sigignore(int sig) Игнорирует sig. Это вспомогательная функция. int sigpause(int sig) Удаляет sig из маски сигналов процесса, а затем приостанавливает процесс до появления сигнала (см. раздел 10.7 «Сигналы для межпроцессного взаимодействия»). sighandler_t sigset(int sig, sighandler_t disp) Это замена для signal (). (Здесь мы использовали обозначение из справочной страницы GNU/Linux, чтобы упростить восприятие объявления функции.) Для использования API требуется компоновка с отдельной библиотекой, -1 j obs. - Примеч. автора.
10.6. Сигналы POSIX 351 Для sigset () аргумент handler может быть SIG_DFL, SIG_IGN или указатель функции, как и для signal (). Однако, он может равняться также и SIG__HOLD. В этом случае sig добавляется к маске сигналов процесса, но связанное с ним действие никак не изменяется. (Другими словами, если бы у него был обработчик, он остается тем же; если было действие по умолчанию, оно не изменяется.) Когда для установки обработчика сигнала используется sigset () и появляется сигнал, ядро сначала добавляет сигнал к маске процессов сигнала, блокируя любой дальнейший прием этого сигнала. Запускается обработчик, а когда он возвращается, ядро восстанавливает маску сигналов процесса в то состояние, какое было до запуска обработчика. (В модели POSIX если обработчик сигнала изменяет маску сигнала, это изменение переписывается в процессе восстановления предыдущей маски, когда обработчик возвращается.) sighold () и sigrelse () могут использоваться совместно для выделения так называемых критических секций кода: участков кода, который не должен прерываться определенным сигналом, чтобы структуры данных не повреждались кодом обработчика сигнала. 13АМЕЧАНИЕ.POSIX стандартизует эти API, поскольку главной целью POSIX Хявляется формализация существующей практики, где это возможно. Однако, функ- \ции sigaction (), которые вскоре будут описаны, дают вам все, что делают эти [API, и даже больше. В новых программах вам не следует использовать эти API. IВместо этого используйте sigaction (). (Мы заметили, что в справочной системе GNU/Linux нет даже страницы для sigsetB)!) 10.6. Сигналы POSIX API POSIX основан на API sigvec () из BSD 4.2 и 4.3. С небольшими изменениями этот API можно было отнести к возможностям API как V7, так и System V Release 3. POSIX сделал эти изменения и переименовал API sigaction (). Поскольку интерфейс sigvec () широко не использовался, мы не будем его описывать. Вместо этого в данном разделе описывается только sigaction (), который вы и должны так или иначе использовать. (На самом деле руководства BSD 4.4 от 1994 г. помечают sigvec () как устаревшую, указывая читателю на sigaction () .) 10.6.1. Обнажение проблемы Что неладно с API System V Release 3? В конце концов, они предоставляют блокирование сигналов, так, что сигналы не теряются, и любой данный сигнал может быть надежно обработан. Ответ в том, что этот API работает лишь с одним сигналом в одно и то же время. Программы обычно обрабатывают больше одного сигнала. И когда вы находитесь в середине процесса обработки одного сигнала, вы не хотите беспокоиться по поводу обработки еще и второго. (Предположим, вы только что начали отвечать по офисному телефону, когда зазвонил ваш мобильный телефон: вы бы предпочли, чтобы телефонная система ответила вызывающему, что в данный момент вы находитесь на другой линии и что скоро освободитесь, вместо того, чтобы проделывать все это самому.)
352 Глава 10. Сигналы С API sigse t () каждый обработчик сигнала должен был бы временно блокировать все другие сигналы, сделать свою работу, а затем разблокировать их. Проблема в том, что в промежутке между любыми двумя вызовами sighold () может появиться еще не заблокированный сигнал. Сценарий, еще раз, распространенный, создающий условия гонки. Решением является обеспечение возможности автоматической работы с группами сигналов, т. е. с помощью одного системного вызова. Вы достигаете этого, работая с наборами сигналов и маской сигналов процесса. 10.6.2. Наборы сигналов: sigset__t и связанные функции Маска сигналов процесса является списком сигналов, которые процесс в настоящее время заблокировал. Сила POSIX API в том, что маской сигналов процесса можно манипулировать атомарно, как единым целым. Маска сигналов процесса программно представляется с помощью набора сигналов. Это тип sigset__t. Концептуально он представляет собой просто битовую маску, причем значения 0 и 1 представляют отсутствие или наличие определенного сигнала в маске: /*' Непосредственное манипулирование маской сигналов. НЕ ДЕЛАЙТЕ ЭТОГО! */ int mask = A « SIGHUP) | A « SIGINT) ; /* битовая маска для SIGHUP и SIGINT */ Однако, поскольку в системе может быть больше сигналов, чем может содержаться в одной int или long и поскольку интенсивное использование побитовых операций тяжело для восприятия, для управления наборами сигналов существует несколько функций API: #include <signal.h> /* POSIX */ int sigemptyset(sigset_t *set); int sigf illset (sigset__t *set) ; int sigaddset (sigset__t *set, int signum) ; int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum); Эти функции следующие: int sigemptyset(sigset_t *set) Освобождает набор сигналов. По возвращении *set не содержит сигналов. Возвращает 0 в случае успеха и -1 при ошибке. int sigfillset(sigset_t *set) Полностью заполняет набор сигналов. По возвращении *set содержит все сигналы, определенные системой. Возвращает 0 в случае успеха и -1 при ошибке. int sigaddset(sigset_t *set, int signum) Добавляет signum к маске сигналов процесса в *set. Возвращает 0 в случае успеха и -1 при ошибке. int sigdelset(sigset_t *set, int signum) Удаляет signum из маски сигналов процесса в *set. Возвращает 0 в случае успеха и -1 при ошибке.
10.6. Сигналы POSIX 353 int sigismember(const sigset_t *set, int signum) Возвращает true/false, если signum присутствует или не присутствует в * set. Перед выполнением с переменной sigset_t каких-то действий всегда следует вызывать одну из функций sigemptyset () или sigf illset (). Существуют оба интерфейса, поскольку иногда бывает нужно начать с пустого набора и работать потом лишь с одним или двумя сигналами, а в другое время бывает нужно работать со всеми сигналами, возможно, убирая один или два сигнала. 10.6.3. Управление маской сигналов: sigprocmask () и др. Маска сигналов процесса вначале пуста - заблокированных сигналов нет. (Это упрощение; см. раздел 10.9 «Сигналы, передающиеся через fork () и exec ().) Три функции позволяют работать непосредственно с маской сигналов процесса: #include <signal.h> /* POSIX */ int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set); int sigsuspend(const sigset_t *set); Функции следующие: int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) Если oldset не равен NULL, получается маска сигналов текущего процесса и помещается в * oldset. Затем маска сигналов процесса обновляется в соответствии с содержимым set и значением how, который должен иметь одно из следующих значений: S IG_BLOCK Объединить сигналы в * set с маской сигналов текущего процесса. Новая маска является объединением текущей маски и *set. SIG__UNBLOCK Удалить сигналы в *set из маски сигналов процесса. Это не представляет проблемы, если *set содержит сигнал, который не содержится в текущей маске сигналов процесса. SIG__SETMASK Заменить маску сигналов процесса содержимым *set. Если set равен NULL, a oldset - нет, значение how неважно. Эта комбинация получает маску сигналов текущего процесса, не меняя ее. (Это явно выражено в стандарте POSIX, но не ясно из справочной страницы GNU/Linux.) int sigpending(sigset__t *set) Эта функция позволяет увидеть, какие сигналы ожидают решения: т. е. *set заполнен этими сигналами, которые были посланы, но они еще не доставлены, поскольку заблокированы. int sigsuspend(const sigset_t *set) Эта функция временно заменяет маску сигналов процесса содержимым *set, а-затем приостанавливает процесс, пока сигнал не будет получен. По определению, заставить функцию вернуться может только сигнал, не находящийся в *set (см. раздел 10.7 «Сигналы для межпроцессного взаимодействия). 12-159
354 Глава 10. Сигналы 10.6.4. Перехват сигналов: sigaction () Наконец мы готовы взглянуть на функцию sigaction (). Эта функция сложна, и мы намеренно опускаем множество деталей, которые предназначены для специального использования. Стандарт POSIX и справочная страница sigaction{2) предоставляют все подробности, хотя вы должны тщательно прочесть и то, и другое, чтобы полностью все усвоить. #include <signal.h> /* POSIX */ int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); Аргументы следующие: int signum Интересующий сигнал, как в случае с другими функциями обработки сигналов, const struct sigaction *act Определение нового обработчика для сигнала signum. struct sigaction *oldact Определение текущего обработчика. Если не NULL, система до установки *act заполняет *oldact. *act может быть NULL, в этом случае *oldact заполняется, но больше ничего не меняется. Таким образом, sigaction () и устанавливает новый обработчик, и получает старый за одно действие, struct sigaction выглядит следующим образом: /* ПРИМЕЧАНИЕ: Порядок в структуре может варьировать. Могут быть также и другие поля! */ - struct sigaction { sigset_t sa_mask; /* Дополнительные сигналы для блокирования */ int sa_flags; /* Контролирует поведение */ void (*sa_handler) (int); /* Может образовать объединение с sa_sigaction */ void (*sa_sigaction) (int, siginfo_t *, void *); /* Монет офазсвать объединение с sajhandler */ } Поля следующие: sigset_t sa_mask Набор дополнительных сигналов для блокирования при запуске функции обработчика. Таким образом, когда вызывается обработчик, общий набор заблокированных сигналов является объединением сигналов в маске процесса, сигналов в act->mask и, если SA_NODEFER сброшен, signum. int sa_flags Флаги, контролирующие обработку сигнала ядром. См. обсуждение далее. void (*sa_handler)(int) Указатель на «традиционную» функцию обработчика. У нее такой же прототип (возвращаемый тип и список параметров), как у функции обработчика для signal (), bsd_signal () nsigset(). void (*sa_sigaction)(int, siginfo_t*, void*) Указатель на функцию обработчика «нового стиля». Функция принимает три аргумента, которые вскоре будут описаны.
10.6. Сигналы POSIX 355 Которая из функций act->sa__handler и act->sa_sigaction используется, зависит от флага SA_SIGINFO в act->sa_f lags. Когда имеется, используется act->sa_sigaction; в противном случае используется act->sa_handler. Как POSIX, так и справочная страница GNU/Linux указывают, что эти два поля могут перекрываться в памяти (т. е. быть частью union). Таким образом, никогда не следует использовать оба поля в одной и той же struct sigaction. Поле sa_f lags составляется с помощью побитового ИЛИ значений одного или более флагов, перечисленных в табл. 10.3. Когда в act->sa_f lags установлен флаг SA_SIGINFO, поле act->sa_sigaction является указателем на функцию, объявленную следующим образом: Таблица 10.3. Значения флагов для sa_f lags Флаг Значение SA_NOCLDSTOP SA_NOCLDWAIT SA_NODEFER SA_NOMASK SA_SIGINFO SA_ONSTACK SA_RESETHAND SA_ONESHOT SA RESTART Этот флаг имеет смысл лишь для SIGCHLD. Когда он установлен, родитель не получает сигнал при остановке порожденного процесса сигналами SIGSTOP, SIGTSTP, SIGTTIN или SIGTTOU. Эти сигналы обсуждаются позже, в разделе 10.8.2. Этот флаг имеет смысл лишь для SIGCHLD. Его поведение сложно. Мы отложим объяснение на потом; см. раздел 10.8.3. Обычно данный сигнал блокируется, когда вызывается обработчик сигнала. Когда установлен один из этих флагов, данный сигнал не блокируется при запуске обработчика. SA__NODEFER является официальным именем POSIX данного флага (которое и следует использовать). Альтернативное имя для SA_NODEFER*. Обработчик сигнала принимает три аргумента. Как упоминалось, при данном установленном флаге должно использоваться поле sa_sigaction вместо sa_handler. Это продвинутая возможность. Обработчики сигналов могут вызываться с использованием предоставленной пользователем памяти в качестве «альтернативного стека сигнала». Эта память дается ядру для подобного использования посредством sigaltstack () (см. sigaltstack{2)). Эта особенность больше не оцисывается в данной книге. Этот флаг обеспечивает поведение V7: после вызова обработчика восстанавливается действие сигнала по умолчанию. Официальным именем POSIX флага (которое следует использовать) является SA_RESETHAND. Альтернативное имя для SA_RESETHAND. Этот флаг предоставляет семантику BSD: системные вызовы, которые могут завершиться с ошибкой EINTR и которые получают этот сигнал, запускаются повторно. Насколько мы смогли определить, имена SA__NOMASK и SA_ONESHOT являются специфическими для GNU/Linux. Если кому-нибудь известно иное, пожалуйста, сообщите нам!
356 Глава 10. Сигналы void action_handler(int sig, siginfo_t *info, void *context) {" /* Здесь тело обработчика */ } Структура siginf o_t предоставляет изобилие сведений о сигнале: /* Определение POSIX 2001. Действительное содеркание макет на разных системах быть разным. */ typedef struct { int si_signo; /* номер сигнала */ int si_errno; /* значение <errno.h> при ошибке */ int si__code; /* код сигнала; см. текст */ pid_t si_pid; /* ID процесса, пославшего сигнал */ uid_t si_uid; /* настоящий UID посылающего процесса */ void *si_addr; /* адрес вызвавшей ошибку инструкции */ int si_status; /* значение завершения, может включать death-by-signal */ long si_band; /* связывающее событие для SIGPOLL/SIGIO */ union sigval si_value; /* значение сигнала (расширенное) */ } siginfo_t; Поля si_signo, si_code и si_value доступны для всех сигналов. Другие поля могут быть членами объединения, поэтому должны использоваться лишь для тех сигналов, для которых они определены. В структуре siginf o_t могут быть также и другие поля. Почти все поля предназначены для расширенного использования. Все подробности содержатся в стандарте POSIX и справочной странице sigactionB). Однако, мы можем описать простое использование поля si_code. Для SIGBUS, SIGCHLD, SIGFPE, SIGILL, SIGPOLL, SIGSEGV и SIGTRAP поле si__code может принимать любое из набора предопределенных значений, специфичных для каждого сигнала, указывая на причину появления сигнала. Откровенно говоря, детали несколько чрезмерны; повседневному коду на самом деле нет необходимости иметь с ними дела (хотя позже мы рассмотрим значения для SIGCHLD). Для всех остальных сигналов член si__code имеет одно из значений из табл. 10.4. Таблица 10.4. Значения происхождения сигнала для si_code Смысл Асинхронный ввод/вывод завершен (расширенный). Сигнал послан ядром. Состояние очереди сообщений изменилось (расширенный.) Сигнал послан из sigqueue () (расширенный). SIGIO поставлен в очередь (расширенный). Значение SI_ASYNCIO SI_KERNEL SI_MESGQ SI_QUEUE SI_SIGIO Только GLIBC V V
10.6. Сигналы POSIX 357 Таблица 10.4. Значения происхождения сигнала для si_code (Продолжение) Значение Только Смысл GLIBC SI_TIMER Время таймера истекло. SI_USER Сигнал послан функцией kill (). raise () и abort () также могут его вызвать, но это не обязательно. В особенности полезно значение SIJJSER; оно позволяет обработчику сигнала сообщить, был ли сигнал послан функциями raise () или kill () (описываются далее). Вы можете использовать эту информацию, чтобы избежать повторного вызова raise () или kill (). Третий аргумент обработчика сигнала с тремя аргументами, void *context, является расширенной возможностью, которая больше не обсуждается в данной книге. Наконец, чтобы увидеть sigaction (') в действии, исследуйте полный исходный код обработчика сигнала для sort .с: 2074 static void 2075 sighandler (int sig) 2076 { 2077 #ifndef SA_NOCLDSTOP /* В системе старого стиля... */ 2078 signal (sig, SIG_IGN) ; /* - для игнорирования sig используйте signal {)*/' 2079 #endif - /* В противном случае sig автоматически блокируется */ 2080 2081 cleanup (); /* Запуск кода очистки */ 2082 2083 #ifdef SA_NOCLDSTOP /* В системе в стиле POSIX... */ 2084 { 2085 struct sigaction sigact; 2086 2087 sigact.sa__handler = SIG_DFL; /* - Установить действие по умолчанию */ 2088 sigemptyset (Scsigact.sajrask); /* - Нет дополнительных сигналов для блокирования */ 2089 sigact ,sa_flags = 0; /* - Специальные действия не предпринимаются */ 2090 sigaction (sig, &sigact, NULL); /* - Поместить на место */ 2091 } 2092 #else /* На системе в старом стиле... */ 2093 signal (sig, SIG_DFL) ,\ /* - Установить действие по умолчанию */ 2094 #endif 2095 2096 raise (sig); /* Повторно послать сигнал */ 2097 } Вот код в main (), который помещает обработчик на свое место: 2214 #ifdef SAJTOCLDSTOP /* На системе POSIX... '*/ 2215 { 2216 unsigned i;
358 Глава 10. Сигналы 2217 sigemptyset (&caught_signals); 2218 for (i = 0; i < nsigs; i++) /* - Блокировать все сигналы */ 2219 sigaddset (&caught_signals, sigs[i]); 2220 newact.sa_handler = sighandler; /* - Функция обработки сигнала */ 2221 newact.sajrask = caught_signals; /* - Установить для офаботчжа маску сигналов процесса */ 2222 newact.sa_flags =0; /* - Особых флагов нет */ 2223 } 2224 #endif ;:'¦¦¦ 2225 2226 { 2227 unsigned i; 2228 for (i =0; i < nsigs; i++) /* Для всех сигналов... */ 2229 { 2230 int sig = sigs[i]; 2231 #ifdef SA_NOCLDSTOP 2232 sigaction (sig, NULL, &oldact); /* - Получить старый обработчик */ 2233 if (oldact.sa_handler != SIG_IGN) /*¦ - Если этот сигнал не игнорируется */ 2234 > sigaction• (sig, &newact, NULL) ; /* - Установить наш обработчик */ 2235 #else 2236 if (signal (sig, SIG_IGN) != SIG_IGN) 2237 signal (sig, sighandler); /* - Та же логика со старым API */ 2238 #endif 2239 } 2240 } Мы заметили, что строки 2216-2219 и 2221 могут быть замещены одним вызовом: sigfillset(& newact.sa_mask); Мы не знаем, почему код написан именно таким способом. Интерес представляют также строки 2233-2234 и 2236-2237, которые показывают правильный способ проверки того, игнорируется ли сигнал, и для установки обработчика лишь в том случае, если сигнал не игнорируется. ХЗАМЕЧАНИЕ. Функции API sigaction () и signalf) не должны использо- Хваться вместе для одного и того же сигнала. Хотя POSIX идет на большие длинно- Хты, чтобы сначала сделать возможным использование signal (), получить I struct sigaction, представляющую диспозицию signal О, и восстановить I ее, все равно это плохая мысль. Код будет гораздо проще читать, писать и пони- Хмать, если вы используете одну функцию или другую взаимоисключающим образом. 10.6.5. Извлечение ожидающих сигналов: sigpending () Описанный ранее системный вызов sigpending () позволяет получить набор ожидающих сигналов, т. е. тех сигналов, которые появились, но еще не доставлены из-за блокировки: #include <signal.h> /* POSIX */ int sigpending(sigset_t *set);
10.6. Сигналы POSIX 359 Помимо разблокировки ожидающих сигналов, чтобы они могли быть доставлены, вы можете решить их игнорировать. Установка действия сигнала SIG_IGN вызывает сбрасывание сигнала (даже если он был заблокирован). Сходным образом для тех сигналов, действием по умолчанию для которых является их игнорирование, установка действия в SIG_DFL также вызывает сбрасывание таких ожидающих сигналов. 10.6.6. Создание возможности для прерьвания функций: siginterrupt () Чтобы сделать определенную функцию прерываемой или повторно запускаемой в зависимости от значения второго аргумента, в качестве удобного средства может использоваться функция siginterrupt (). Объявление следующее: #include <signal.h> /* XSI */ int siginterrupt(int sig, int flag); В соответствии со стандартом POSIX поведение siginterrupt () эквивалентно следующему коду: int siginterrupt(int sig, int flag) { int ret; struct sigaction act; (void) sigaction(sig, MJLL, &act); /* Получить старые установки */ if (flag) /* Если flag равен true... */ act.sa_flags &= ~SA_RESTART; /* Запретить повторный запуск */ else /* В противном случае... */ act.sa_flags |= SA_RESTART; /* Разрешить повторный запуск */ ret = sigaction (sig, &act, NULL); /* Поместить новые установки на место */ return ret; /* Вернуть результат */ } В случае успеха возвращаемое значение равно 0 и -1 при ошибке. 10.6.7. Передача сигналов: kill () и killpg () Традиционная функция Unix для передачи сигналов называется kill(). Имя несколько неправильное; все, что она делает - отправляет сигнал. (Результатом этого часто является завершение получателя сигнала, но это не обязательно верно. Однако, теперь слишком поздно менять имя.) Функция killpg () посылает сигнал определенной группе процессов. Объявления следующие: #include <sys/types.h> * /* POSIX */ #include <signal.h> int kill(pid_t pid, int sig); int killpg(int pgrp, int sig); /* XSI */
360 Глава 10. Сигналы pid pid pid > = = 0 0 -1 Аргумент sig является либо именем сигнала, либо 0. В последнем случае сигнал не посылается, но ядро все равно осуществляет проверку ошибок. В частности, это правильный способ проверки существования данного процесса или группы, а также проверки того, что у вас есть разрешение на передачу сигналов процессу или группе процессов. kill () возвращает 0 в случае успеха и -1 при ошибке; errno указывает на проблему. Правила для значения pid несколько запутаны: pid является номером процесса, и сигнал посылается этому процессу. Сигнал посылается каждому процессу в группе посылающего процесса. Сигнал посылается каждому процессу в системе, за исключением специальных системных процессов. Применяется проверка прав доступа. На системах GNU/Linux исключается лишь процесс init (PID l), но у других систем могут быть другие специальные процессы. pid < -1 Сигнал посылается группе процессов, представленной абсолютным значением pid. Таким образом, вы можете отправить сигнал всей группе процессов, дублируя возможности killpg.(). Эта неортогональность обеспечивает историческую совместимость. Значение pid для kill () сходно со значением для waitpid () (см. раздел 9.1.6.1 «Использование функций POSIX: wait () и waitpid () »). Стандартная функция С raise () в сущности эквивалентна int raise(int sig) { return kill(getpid(), sig); } Комитет по стандартизации С выбрал имя raise (), поскольку С должен работать таюке в окружениях, не относящихся к Unix, a kill () была сочтена специфичной для Unix функцией. Представилась также возможность дать этой функции более описательное имя. killpg () посылает сигнал группе процессов. Пока значение pgrp превышает 1, эта функция эквивалентна 'kill (-pgrp, sig)'. Справочная страница GNU/Linux killpg{2) утверждает, что если pgrp равно 0, сигнал посылается группе отправляющего процесса. (Это то же самое, что и kill ().) Как вы могли представить, нельзя послать сигнал произвольному процессу (если вы не являетесь суперпользователем, root). Для обычных пользователей действительный или эффективный UID отправляющего процесса должен соответствовать действительному или сохраненному set-user-ID получающего процесса. (Различные UID описаны в разделе 11.1.1 «Действительные и эффективные ID».) Однако SIGCONT является особым случаем: пока получающий процесс является членом того же сеанса, что и отправляющий, сигнал пройдет. (Сеансы были кратко описаны в разделе 9.2.1 «Обзор управления заданиями».) Это особое правило позволяет управляющей заданиями оболочке продолжать остановленные процессы-потомки, даже если этот остановленный процесс имеет другой ID пользователя.
10.7. Сигналы для межпроцессного взаимодействия 361 10.6.8. Наша история до настоящего времени, эпизод II System V Release 3 API был предназначен для исправления различных проблем, представленных первоначальным API сигналов V7. В частности, важной дополнительной концепцией является понятие о блокировке сигналов. Однако, этот API оказался недостаточным, поскольку он работал лишь с одним сигналом за раз, оставляя множество широко открытых окон, через которые могли поступать нежелательные сигналы. POSIX API, работая атомарно с множеством сигналов (маской сигналов процесса, программно представленной типом sigset__t), решает эту проблему, закрывая окна. Первый набор функций, который мы исследовали, манипулирует значениями sigset_t: sigf illset (), sigemptyset (), sigaddset (), sigdelset (.) и sigismember (). Следующий набор работает с маской сигналов процесса: sigprocmask() устанавливает и получает маску сигналов процесса, sigpending () получает набор ожидающих сигналов, a sigsuspend () помещает процесс в состояние сна, временно заменяя маску сигналов процесса одним из своих параметров. Функция POSIX API sigaction () (весьма) запутана из-за необходимости обеспечить обратную совместимость: SA_RESETHAND и SA_RESTART в поле sa__f lags; выбор, блокировать также полученный сигнал или нет: SA_NODEFER для sa_f lags; • возможность иметь два различных вида обработчиков сигналов: с одним или с тремя аргументами; выбор поведения для управления SlGCHLD: SA_NOCLDSTOP и SA_NOCLDWAIT для sa_flags. Функция siginterrupt () является удобной для разрешения или запрещения повторного запуска системных вызовов для данного сигнала. Наконец, для посылки сигналов не только текущему, но также и другим процессам могут использоваться kill () и killpg () (конечно, с проверкой прав доступа). 10.7. Сигналы для межпроцессного взаимодействия «ЭТО УЖАСНАЯ МЫСЛЬ! СИГНАЛЫ НЕ ПРЕДНАЗНАЧЕНЫ ДЛЯ ЭТОГО! Просто скажите НЕТ». - Джефф Колье (Geoff Collyer) - Одним из главных механизмов межпроцессного взаимодействия (IPC) являются каналы, которые описаны в разделе 9.3 «Базовая межпроцессная коммуникация: каналы и FIFO». Сигналы также можно использовать для очень простого IPC6. Это довольно грубо; получатель может лишь сказать, что поступил определенный сигнал. Хотя функция Наша благодарность Ульриху Дрепперу (Ulrich Drepper) за помощь в разъяснении, связанных с этим проблем. - Примеч. автора.
362 Глава 10. Сигналы sigaction () позволяет получателю узнать PID и владельца процесса, пославшего сигнал, эти сведения обычно не очень помогают. [ЗАМЕЧАНИЕ. Как указывает цитата в начале, использование сигналов для IPC {почти всегда является плохой мыслью. Мы рекомендуем по возможности избегать I этого. Но нашей целью является научить вас, как использовать возможности \ Linux/Unix, включая их отрицательные моменты, оставляя за вами принятие [информированногорешения, что именно использовать. Сигналы в качестве IPC для многих программ могут быть иногда единственным выбором. В частности, каналы не являются альтернативой, если две взаимодействующие программы не запущены общим родителем, а файлы FIFO могут не быть вариантом, если одна из взаимодействующих программ работает лишь со стандартными вводом и выводом. (Примером обычного использования сигналов являются определенные системные программы демонов, таких, как xinetd, которые принимают несколько сигналов, уведомляющих, что нужно повторно прочесть файл настроек, осуществить проверку непротиворечивости и т. д. См xinetd(S) в системе GNU/Linux и inetd(S) в системе Unix.) Типичная высокоуровневая структура основанного на сигналах приложения выглядит таким образом: for(;;){ /* Ожидание сигнала */ /* Обработка сигнала */ } Оригинальным интерфейсом V7 для ожидания сигнала является pause (): #include <unistd.h> /* POSIX */ int pause(void); pause () приостанавливает процесс; она возвращается лишь после того» как сигнал будет доставлен и его обработчик вернется из вызова. По определению, pause () полезна лишь с перехваченными сигналами - игнорируемые сигналы при их появлении игнорируются, а сигналы с действием по умолчанию, завершающим процесс (с созданием файла образа или без него), продолжают действовать так же. Проблема в только что описанной высокоуровневой структуре приложения кроется в части «Обработка сигнала». Когда этот код запускается, вы не захотите обрабатывать другой сигнал; вы хотите завершить обработку текущего сигнала до перехода к следующему. Одним из возможных решений является структурирование обработчика сигнала таким образом, что он устанавливает флаг и проверяет его в главном цикле: volatile sig__atomicJ: signal_w&iting = 0; /* true, если не обрабатываются сигналы */ void handler(int sig) {• signal_waiting = 1; . /* Установка других данных, указывающих вид сигнала */ }
10.7. Сигналы для межпроцессного взаимодействия 363 В основном коде флаг проверяется: for ( ; ; ) { if (! signal_waiting) { /* Если возник другой сигнал, */ pauseO; /* этот код пропускается */ signal_waiting = 1; } /* Определение поступившего сигнала */ signal_waiting = 0; /* Обработка сигнала */ } К сожалению, этот код изобилует условиями гонки: for (;;) { if (! signal_waiting) { /* < Сигнал может появиться здесь, после проверки условия! */ pause(); /* pause() будет вызвана в любом случае */ signal_waiting = 1; } /* Определение поступившего сигнала < Сигнал может переписать здесь глобальные данные V signal__waiting = 0; /* Обработка сигнала < То же и здесь, особенно для нескольких сигналов */ } Решением является блокирование интересующего сигнала в любое время, кроме ожидания его появления. Например, предположим, что интересующим нас сигналом является SIGINT: void handler(int sig) { /* sig автоматически блокируется функцией sigaction() */ ./* Установить глобальные данные, касающиеся этого сигнала */ } int main(int argc, char **argv) { sigset__t set; struct sigaction act; /* ...обычная настройка, опции процесса и т.д. ... */¦ sigemptyset(& set); /* Создать пустой набор */ sigaddset(& set, SIGINT); /* Добавить в набор SIGINT */ sigprocmask(SIG_BLOCK, & set, NULL); /* Заблокировать его */ act.sa_mask = set; /* Настроить обработчик */ act.sa_handler = handler; act.sa_flags = 0;
364 Глава 10. Сигналы sigaction(sig, & act, NULL); /* Установить его */ ... /* Возможно, установить отдельные обработчики */ ... /* для других сигналов */ sigemptyset(& set); /* Восстановить пустой, допускает SIGINT */ for (;;){. sigsuspend(& set); /* Ждать появления SIGINT */ /* Обработка сигнала. SIGINT здесь снова блокируется */ } /* ...любой другой код... */ return 0; } Ключом к использованию этого является то, что sigsuspend() временно заменяет маску сигналов процесса маской, переданной в аргументе. Это дает SIGINT возможность появиться. При появлении он обрабатывается; обработчик сигнала возвращается, а вслед за ним возвращается также sigsuspend()'. Ко времени возвращения sigsuspendl) первоначальная маска процесса снова на месте. Вы легко можете расширить этот пример для нескольких сигналов, блокируя в main () и в обработчике все интересующие сигналы и разблокируя их лишь в вызове sigsuspended(). При наличии всего этого не следует в новом коде использовать pause (). pause () был стандартизован POSIX главным образом для поддержки старого кода. То же самое верно и для функции sigpause () System V Release 3. Вместо этого, если нужно структурировать свое приложение с использованием сигналов для IPC, используйте исключительно функции API sigsuspend () и sigaction (). I ЗАМЕЧАНИЕ, Приведенный выше код предполагает, что маска сигналов процесса начинается пустой. Код изделия должен вместо этого работать с любой маской сигналов, имеющейся на момент запуска программы. 10.8. Важные сигналы специального назначения Некоторые сигналы имеют особое назначение. Здесь мы опишем наиболее важные. 10.8.1. Сигнальные часы: sleep (), alarm() и SIGALARM Часто бывает необходимо написать программу в виде while (/* некоторое неверное условие */){ /* подождать некоторое время */ }
10.8. Важные сигналы специального назначения 365 Часто такая потребность возникает в сценариях оболочки, например, в ожидании регистрации определенного пользователя: until who | grep *Aarnold' > /dev/null do sleep 10 done Два механизма, один низкоуровневый, другой высокоуровневый, позволяют работающему процессу узнать, когда истекло заданное количество секунд. 10.8.1.1. Труднее, но с большим контролем: alarm () и sigalarm Основным строительным блоком является системный вызов alarm (): #include <unistd.h> /* POSIX */ unsigned int alarm(unsigned int seconds); После того, как alarm () возвратится, программа продолжает работать. Однако, когда истекают seconds секунд, ядро посылает процессу SIGALARM. Действием по умолчанию является завершение процесса, но вы скорее всего вместо этого установите обработчик сигнала для SIGALARM. Возвращаемое значение либо 0, либо, если был установлен предыдущий сигнальный интервал, число секунд, остающихся до его завершения. Однако, для процесса имеется лишь один такой сигнальный интервал; предыдущий отменяется, а новый помещается на его место. Преимуществом здесь является то, что со своим установленным обработчиком вы можете делать при поступлении сигнала все, что хотите. Недостаток же в том, что приходится быть готовым к работе в нескольких контекстах: основном контексте и контексте обработчика сигнала. 10.8.1.2. Простой и легкий: si еер () Более легкий способ ожидания истечения фиксированного промежутка времени заключается в использовании функции sleep (): #include <unistd.h> /* POSIX */ unsigned int sleep(unsigned int seconds); Возвращаемое значение равно 0, если процесс проспал все отведенное время. В ррр- тивном случае возвращается оставшееся для сна время. Это последнее значение может возникнуть в случае, если появился сигнал, пока процесс дремал. ХЗАМЕЧАНИЕ. Функция sleep () часто реализуется через сочетание signal (), Ialarm () и pause (). Такой подход делает опасным смешивание sleep (.) с вашим {собственным вызовом alarm() (илирасширенной функцией setitimerO, описан- \ной в разделе 14.3.3 «Интервальные таймеры: setitimerO и getitimerf)»). I Чтобы теперь узнать о функции nano sleep (), см. раздел J 4.3.4 «Более точные пау- \зы: nanosleepO ».
366 Глава 10. Сигналы 10.8.2. Сигналы, управляющие заданиями Несколько сигналов используются для реализации управления заданиями - возможностью начинать и останавливать задания и перемещать их из фонового режима на передний план и обратно. На уровне пользователя вы, несомненно, проделывали это: использовали CTRL-Z для остановки задания, bg для помещения его в фоновый режим, а иногда использовали fg для перемещения фонового или остановленного задания на передний план. Секция 9.2.1 «Обзор управления заданиями» описывает в общем, как осуществляется управление заданиями. Данный раздел завершает обзор, описав сигналы управления заданиями, поскольку иногда может понадобиться перехватить их непосредственно: SIGTSTP Этот сигнал осуществляет «остановку терминала». Это сигнал, который ядро посылает процессу, когда пользователь за терминалом (или окном, эмулирующим терминал) набирает определенный ключ. Обычно это CTRL-Z, аналогично тому, как CTRL-C обычно посылает Sib INT. Действием по умолчанию для SIGTSTP является остановка (переход в приостановленное состояние) процесса. Однако, вы можете перехватить этот сигнал, как любой другой. Хорошая мысль сделать это, если ваша программа изменяет состояние терминала. Например, рассмотрите экранные редакторы vi или Emacs, которые переводят терминал в посимвольный режим. По получении SIGTSTP, они должны восстановить терминал в его нормальный построчный режим, а затем приостановиться сами. SIGSTOP Этот сигнал также останавливает процесс, но он не может быть перехвачен, заблокирован или проигнорирован. Он может быть использован в качестве последнего средства вручную (посредством команды kill) или программным путем. Например, только что обсужденный обработчик SIGTSTP после восстановления состояния терминала мог бы затем использовать для остановки процесса 'raise (SIGSTOP)'. SIGTTIN, SIGTTOU Ранее эти сигналы были определены как «фоновое чтение из tty» и «фоновая запись в tty». tty является устройством терминала. В системах управления заданиями процессы, работающие в фоновом режиме, заблокированы от попыток чтения с терминала или записи в него. Когда процесс пытается осуществить любую из этих операций, ядро посылает ему соответствующий сигнал. Для обоих действием по умолчанию является остановка процесса. При желании можно перехватить эти сигналы, но для этого редко бывает необходимость. SIGCONT Этот сигнал вновь запускает остановленный процесс. Если процесс не остановлен, он игнорируется. При желании его можно перехватить, но опять-таки для большинства программ мало причин для осуществления этого. Продолжая наш пример, обработчик SIGCONT для экранного редактора должен перед возвращением вернуть терминал обратно в посимвольный режим.
10.8. Важные сигналы специального назначения 367 Когда процесс остановлен, любые другие посланные ему сигналы становятся ожидающими. Исключением является SIGKILL, который всегда доставляется процессу и который не может быть перехвачен, заблокирован или проигнорирован. В предположении, что были посланы сигналы кроме SIGKILL, по получении SIGCONT ожидающие сигналы доставляются, а процесс продолжает выполнение после того, как они будут обработаны. 10.8.3. Родительский надзор: три различные стратегии Как описано в разделе 9.1.1 «Создание.процесса: fork ()», одним побочным эффектом вызова fork () является создание между процессами отношений родитель-потомок. Родительский процесс может ждать завершения одного или более из своих потомков и получить статус завершения порожденного процесса посредством одного из семейства системных вызовов wait (). Завершившиеся порожденные процессы, которых никто не ожидал, называются зомби (zombies). Обычно каждый раз при завершении порожденного процесса ядро посылает родительскому процессу сигнал SIGCHLD7; Действием по умолчанию является игнорирование этого сигнала. В этом случае процессы зомби накапливаются до тех пор, пока родитель не вызовет wait () или не закончится сам. В последнем случае процессы зомби получают в качестве нового родителя системный процесс init (PID 1), который получает от них результаты как часть своей обычной работы. Сходным образом, активные потомки также получают родителем init, и их результаты будут собраны при их завершении. SIGCHLD используется для большего, чем уведомление о завершении потомка. Каждый раз при остановке потомка (посредством одного из обсужденных ранее сигналов управления заданиями) родителю также посылается SIGCHLD. Стандарт POSIX указывает, что SIGCHLD «может быть послан» также, когда помок вновь запускается; очевидно, среди оригинальных Unix-систем имеются различия. Сочетание флагов для поля sa_flags в struct sigation и использование SIG__IGN в качестве действия для SIGCHLD позволяет изменить способ обработки ядром остановок, возобновления или завершения потомков. Как и с сигналами в общем, описанные здесь интерфейсы и механизмы сложны, поскольку они развивались с течением времени. 10.8.3.1. Плохие родители: полное игнорирование потомков Простейшим действием, которое вы можете сделать, является изменение действия для SIGCHLD на SIG__IGN, В этом случае завершившиеся потомки не становятся зомби. Вместо этого статус их завершения отбрасывается, и они полностью удаляются из системы. Исторически системы BSD использовали имя SIGCHLD, которое используется и POSIX. В System V есть сходный сигнал с именем SIGCLD. GNU/Linux определяет последний через #define как первый - см. табл. 10.1. - Примеч. автора.
368 Глава 10, Сигналы Другой возможностью, дающей такой же результат, является использование флага SA_NOCLDWAIT. В коде: /* Старый стиль: */ /* Новый стиль: */ signal(SIGCHLD, SIG_IGN); struct sigaction sa; sa.sa_handler = SIG__IGN;. sa.sa_flags = SA_NOCLDWAIT; sigeinptyset (& sa.sa_mask); sigaction(SIGCHLD, & sa, NULL); 10.8.3.2. Снисходительные родители: минимальный надзор В качестве альтернативы можно беспокоиться лишь о завершении потомка и не интересоваться простыми изменениями состояния (остановке и возобновлении). В этом случае используйте флаг SA_NOCLDSTOP и установите обработчик сигнала, вызывающий wait () (или родственную ей функцию) для получения данных процесса. В общем вы не можете ожидать получать по одному сигналу SIGCHLD на каждого завершающегося потомка. Следует считать, что SIGCHLD означает «завершился по крайней мере один потомок» и быть готовым собрать при обработке SIGCHLD сведения о как можно большем числе потомков. Следующая программа, chlO-reapl .с, блокирует SIGCHLD до тех пор, пока не будет готова восстановить потомков. I /* chlO-reapl.c демонстрирует управление SIGCHLD с использованием цикла */ 2 3 #include <stdio.h> 4 #include <errno.h> 5 #include <signal.h> 6 #include <string.h> 7 #include <sys/types.h> 8 #include <sys/wait.h> 9 10 #define MAX_KIDS 42 II #define NOT_USED -1 • 12 13 pid_t kids[MAX_KIDS]; 14 size_t nkids = 0; Массив потомков отслеживает ID порожденных процессов. Если элемент содержит NOTMJSED, он не представляет необработанного потомка. (Его инициализируют строки 89-90 внизу.) nkids указывает, сколько значений в kids следует проверить. 16 /* format_jium — вспомогательная функция, поскольку нельзя использовать [sf ]printf () */ 17 . ¦ ... 18 const char *format_num(int num)
10.8. Важные сигналы специального назначения 369 19 { 20 #define NUMSIZ 30 21 static char buf[NUMSIZ]; 22 int i; 23 24 if (num <= 0) { 2 5 strcpy(buf, "); 2 6 return, buf; 27 } 28 29 i = NUMSIZ - 1; 30 buf[i--] = 'XO'; 31 32 /* Преобразует цифры обратно в строку. */ 33 do { 34 buf[i—-] = (num % 10) + ' 0 ' ; 35 num /= 10; 3 6 } while (num > 0) ; 37 3 8 return Sc buf [i + 1]'; 39 } Поскольку обработчики сисналов не должны вызывать функции семейства print f (), мы предусмотрели для преобразования десятичного сигнала или номера РГО в строку простую «вспомогательную» функцию f ormat_num (). Это примитивно, но работает. 41 /* childhandler перехват SIGCHLD, сбор сведений со всех доступных потомков */ 42 43 void childhandler(int sig) 44 { 45 int status, ret; 46 int i; 47 char buf[100]; 48 static const char entered[] = "Entered childhandler\n"; 49 static const char exited[] = "Exited childhandler\n"; 50 , 51 writed, entered, strlen (entered) ) ; 52 for (i = 0; i < nkids; i++) { 53 if (kids[i] == NOT_USED) 54 continue; 55 56 retry: 57 if ((ret = waitpid(kids[i] , & status, WNOHANG)) =='kids[i]) { 58 strcpy(buf, "\treaped process "); 59 strcat(buf, format_num(ret));
370 Глава 10. Сигналы 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 } } } } } strcat(buf, "\n"); write(l, buf, strlen(buf)); kids[i] = NOT_USED; else if (ret == 0) { strcpy(buf/ "\tpid "); streat(buf, format_jium(kids [i] ) ) ; strcat(buf, " not available yet\n"); " writeA, buf, strlen(buf)); else if (ret == -1 && errno == EINTR) { writeA, "\tretrying\n", 10); goto retry; else { strcpy(buf, " \twaitpid() failed: " )•; strcat(buf, strerror(errno)); strcat(buf, "\n"); writed, buf, strlen(buf) ) ; write(l, exited, strlen(exited)); } Строки 51 и 58 выводят «входное» и «завершающее» сообщения, так что мы можем ясно видеть, когда вызывается обработчик сигнала. Другие сообщения начинаются с ведущего символа TAB. Главной частью обработчика сигнала является большой цикл, строки 52-77. Строки 53-54 проверяют на NOTMJSED и продолжают цикл, если текущий слот не используется. Строка 57 вызывает waitpid () с PID текущего элемента kids. Мы предусмотрели опцию WNOHANG, которая заставляет wai tpid () возвращаться немедленно, если затребованный потомок недоступен. Этот вызов необходим, так как возможно, что не все потомки завершились. Основываясь на возвращенном значении, код предпринимает соответствующее действие. Строки 57-62 обрабатывают случай обнаружения потомка, выводя сообщение и помещая в соответствующий слот в kids значение NOT_USED. Строки 63-67 обрабатывают случай, когда затребованный потомок недоступен. В этом случае возвращается значение 0, поэтому выводится сообщение, и выполнение продолжается. Строки 68-70 обрабатывают случай, при котором был прерван системный вызов. В этом случае самым подходящим способом обработки является goto обратно на вызов waitpid(). (Поскольку main() блокирует все сигналы при вызове обработчика сигнала [строка 96], это прерывание не должно случиться. Но этот пример показывает, как обработать все случаи.) Строки 71-76 обрабатывают любую другую ошибку, выводя соответствующее сообщение об ошибке.
10.8. Важные сигналы специального назначения 371 81 /* main установка связанных с порожденными процессами сведений и сигналов, создание порожденных процессов */ 82 83 int main(int argc, char **argv) 84 { 85 struct sigaction sa; 86 sigset_t childset, emptyset; 87 int i; 88 89 for (i = 0; i < nkids; i++) 90 kids[i] = NOT__USED; 91 92 sigemptyset(& emptyset); 93 94 sa.sa_flags =^SA.NOCLDSTOP; 95 sa.sa_handler'= childhandler; 96 sigfillset(& sa.sa_mask); /* блокировать все при вызове обработчика */ 97 sigaction(SIGCHLD, & sa, NULL); 98 99 sigemptyset(& childset); 100 sigaddset(& childset, SIGCHLD); 101 102 sigprocmask(SIG_SETMASK, & childset, NULL); /* блокировать его в коде main */ 103 104 for (nkids = 0; nkids < 5; nkids++) { 105 if ( (kids [nkids] = fdrkO) == 0) { 106 sleep C)"; 107 _exit@); 108 } 109 } 110 111 sleepE); /* дать потомкам возможность завершения */ 112 113 printf("waiting for signal\n"); -114 sigsuspend(& emptyset); 115 116 return 0; 117 } Строки 89-90 инициализируют kids. Строка 92 инициализирует emptyset. Строки 94-97 настраивают и устанавливают обработчик сигнала для SIGCHLD. Обратите внимание на использование в строке 94 SA_NOCLDSTOP, тогда как строка 96 блокирует все сигналы при вызове обработчика.
372 Глава 10. Сигналы Строки 99-100 создают набор сигналов, представляющих SIGCHLD, а строка 102 устанавливает их в качестве маски сигналов процесса для программы. Строки 104-109 создают пять порожденных процессов, каждый из которых засыпает на три секунды. По ходу дела они обновляют массив kids и переменную nkids. Строка 111 дает затем потомкам шанс завершиться, заснув на еще больший промежуток времени. (Это не гарантирует, что порожденные процессы завершатся, но шансы довольно велики.) Наконец, строки 113-114 выводят сообщение и приостанавливаются, заменив маску сигналов процесса, блокирующую SIGCHLD, пустой маской. Это дает возможность появиться сигналу SIGCHLD, что в свою очередь вызывает запуск обработчика сигнала. Вот что происходит: $ chlO-reapl /* Запуск программы*/ waiting for signal Entered childhandler reaped process 23937 reaped process 23938 reaped process 23939 reaped process 23940 reaped process 23941 Exited childhandler Обработчик сигнала собирает сведения о потомках за один проход. Следующая программа, chl0-reap2 .с, сходна с chlO-reapl .с. Разница в том, что она допускает появление сигнала SIGCHLD в любое время. Такое поведение увеличивает шанс получения более одного SIGCHLD, но не гарантирует это. В результате обработчик сигнала все равно должен быть готов обработать в цикле несколько потомков. 1 /* chl0-reap2.c демонстрирует управление SIGCHLD, один сигнал на потомка */ 2 /* ...не изменившийся код пропущен... */ 12 13 pid_t kids[MAX_KIDS]; 14 size_t nkids = 0; 15 size_t kidsleft =0; /* <<< Добавлено */ 16 • /* ...не изменившийся код пропущен... */ 41 42 /* childhandler перехват SIGCHLD, опрос всех доступных потомков */ 43 44 void childhandler(int sig) 45 { 46 int status, ret; 47 int i; 48 char buf[100];
10.8. Важные сигналы специального назначения 373 49 static const char entered[] = "Entered childhandler\n"; 50 static const char exited[] = "Exited childhandler\n"; 51 52 write(l, entered, strlen(entered)); 53 for (i = 0; i < nkids; i++) . { 54 if (kids[i] == NOT_USED) 55 continue; 56 57 retry: 58 if ((ret = waitpid(kids[i], & status, WNOHANG)) == kids[i]) {. ' 59 strcpy(buf, "\treaped process "); 60 strcat(buf, format_num(ret)); 61 strcat(buf, "\n"); 62 writed, buf, strlen(buf)); 63 kids[i] = NOT_USED; 64 kidsleft--; /* <<< Добавлено */ 65 } else if (ret == 0) { /* ...не изменившийся код пропущен... */ 80 writed, exited, strlen (exited) ) ; 81 } Это идентично предыдущей версии за тем исключением, что у нас есть новая переменная, kidslef t, указывающая, сколько имеется не опрошенных потомков. Строки 15 и 64 помечают новый код. 83 /* main установка относящейся к порожденным процессам сведений и сигналов, создание порожденных процессов */ 84 85 int main(int argc, char **argv) 86 { /* ...не изменившийся код пропущен.. . */ 100 101 sigemptyset(& childset); 102 sigaddset(& childset, SIGCHLD); 103 104 /* sigproamsk(SIG_SETMASK, & childset, NULL); /* блокирование в коде main */ 105 106 for (nkids = 0; nkids < 5; nkids++) { 107 if ( (kids [nkids] = forkO) == 0) { 108 sleepC); 109 _exit@); 110 } 111 kidsleft++; /* <« Added */ 112 } 113
374 Глава 10. Сигналы 114 /* sleepE); /* дать потомкам шанс завершиться */ 115 116 while (kidsleft > 0) { /* <« Добавлено */. 117 printf("waiting for signals\n"); 118 sigsuspend(& emptyset); 119 } /* <« Добавлено */ 120 121 return 0; 122 } Здесь код также почти идентичен. Строки 104 и 114 закомментированы из предыдущей версии, а строки 111,116 и 119 добавлены. Удивительно, при запуске поведение меняется в зависимости от версии ядра! $ uname -а /* Отобразить версию системы */ Linux examplel 2.4.20-8 #1 Thu Mar 13 17:54:28 EST 2003 i686 i686 i386 GNU/Linux $ chl0-reap2. /* Запустить программу */ waiting for signals Entered childhandler /* Опрос одного потомка */ reaped process 2702 pid 2703 not available yet pid 2704 not available yet pid 2705 not available yet pid 2706 not available yet Exited childhandler waiting for signals Entered childhandler /* И следующего */ reaped process 2703 pid 2704 not available yet pid 2705 not available yet pid 2706 not available yet Exited childhandler •waiting for signals Entered childhandler /* И так далее */ reaped process 2704 pid 2705 not available yet pid 2706 not available yet Exited childhandler waiting for signals Entered childhandler reaped process 2705 pid 2706 not available yet Exited childhandler waiting for signals
10.8. Важные сигналы специального назначения 375 Entered childhandler reaped process 2706 Exited childhandler В данном примере на каждый процесс поступает ровно один SIGCHLD! Хотя это прекрасно и полностью воспроизводимо на этой системе, это также необычно. Как на более раннем, так и на более позднем ядре и на Solaris программа получает один сигнал для более чем одного потомка: $ uname -a /* Отобразить версию системы */ Linux example2 2.4.22-1.2115.nptl #1 Wed Oct 29 15:42:51 EST 20,03 i686 i686 i386 GNU/Linux $ chl0-reap2 • /* Запуск программы */ waiting for signals Entered childhandler /* Обработчик сигнала вызван лишь однажды */ reaped process 9564 reaped process 9565 reaped process 9566 reaped process 9567 reaped process 9568 Exited childhandler \ ЗАМЕЧАНИЕ. В коде для chl0-reap2. с есть один важный дефект- состояние I гонки. Взгляните еще раз на строки 106-112 в chl0-reap2. с. Что случится, если I SIGCHLD появится при исполнении этого кода? Массив kids.и переменные nkids I и kidsleftмогут оказаться разрушенными: код в main добавляет новый процесс, I но обработчик сигнала вычитает один. I Этот пример кода является отличным примером критического раздела; он не дол- \жен прерываться при исполнении. Правильным способом работы с этим кодом Хявляется заключение его между вызовами, которые сначала блокируют, а затем разблокируют SIGCHLD. 10.8.3.3. Строгийродительскийконтроль Структура siginf o_t и перехватчик сигнала с тремя аргументами дают возможность узнать, что случилось с потомком. Для SIGCHLD поле si_code структуры siginf o__t указывает причину посылки сигйала (остановка, возобновление, завершение порожденного процесса и т. д.). В табл, 10.5 представлен полный список значений. Все они определены в качестве расширения XSI стандарта POSIX. Следующая программа, chlO-status . с, демонстрирует использование структуры siginf o_t. 1 /* chlO-status.с — демонстрирует управление SIGCHLD, используя обработчик с 3 аргументами */ 2 3 #include <stdio.h> 4 #include <errno.h> 5 #include <signal.h>
376 .Глава iO. Сигналы 6 #include <string.h> 7 #include <sys/types.h> 8 #i,nclude <sys/wait.h> 9 10 void manage(siginfo_t *si) ; 11 /*•...не изменившийся для format_num() код опущен... */ Таблица 10.5. Значения si__code XSI для SIGCHLD Значение Смысл CLD_CONTINUED Остановленный потомок был возобновлен. CLD_DUMPED Потомок завершился с ошибкой, создан образ процесса. CLD_EXITED Потомок завершился нормально. CLD__KILLED Потомок был завершен сигналом. CLD_STOPPED Порожденный процесс был остановлен. CLD__TRAPPED Трассируемый потомок остановлен. (Это условие возникает, когда программа трассируется - либо из отладчика, либо для мониторинга реального времени. В любом случае, вы вряд ли увидите его в обычных ситуациях.) Строки 3-8 включают стандартные заголовочные файлы, строка 10 объявляет manage (), которая иМеет дело с изменениями состояния потомка, а функция forma t___num () не изменилась по сравнению с предыдущим. 37 /* childhandler перехват SIGCHLD, сбор данных лишь об одном потомке */ 38 39 void childhandler(int sig, siginfo_t *si, void *context) 40 { 41 int status, ret; 42 int i; 43 char buf[100] ; 44 static const char entered[] = "Entered childhandler\n"; 45 static const char exited[] = "Exited childhandler\n"; 46 47 write(l, entered, strlen(entered)); 48 retry: 49 if ((ret = waitpid(si->si__pid, & status, WNOHANG) ) == si->si_pid) { 50 ' strcpy(buf, "\treaped process "); 51 strcat(buf, format._num(si->si_pid) ) ; 52 strcat(buf, "\n"); 53 writeA, buf, strlen(buf)); 54 manage(si); /* обработать то, что произошло */ v
.10.8. Важные сигналы специального назначения 377 55 } else if (ret > 0) { 56 strcpy(buf, "\treaped unexpected pid ")/ 57 strcat(buf/ format_num(ret)); 58 strcat(buf, "\n"); 59 writed, buf, strlen(buf)); 60 goto retry; /* почему .бы нет? */ 61 } else if (ret == 0) { 62 strcpy(buf, "\tpid "); 63 strcat(buf, format_num(si->si_pid));. 64 strcat(buf, " changed status\n"); 65 writeA, buf, strlen(buf)); 66 manage(si); /* обработать то, что произошло */ 67 } else if (ret == -1 && errno == EINTR) { 68 writell,'"\tretrying\n", 10); 69 goto retry; 70 } else { 71 strcpy(buf, "\twaitpid() failed: ") ; 72 strcat(buf, strerror(errno)); 73 strcat(buf, "\n"); 74 writeA, buf, strlen(buf)); 75 } 76 77 writed, exited, strlen (exited) ) ; 78 } Обработчик сигнала похож на показанные ранее. Обратите внимание на список аргументов (строка 39) и на то, что нет цикла. Строки 49-54 обрабатывают завершение процесса, включая вызов manage () для вывода состояния. Строки 55-60 обрабатывают случай неожиданного завершения потомка. Этого не должно происходить, поскольку обработчику сигнала передается специфическая для определенного порожденного процесса информация. Строки 61-66 представляют для нас интерес: возвращаемое значение для изменений состояния равно 0. manage () имеет дело с деталями (строка 66). Строки 67-69 обрабатывают прерывания, а строки 70-75 распоряжаются ошибками. 80 /* child что сделать в порожденном процессе */ 81 ¦ ' . 82 void child(void) 83 { 84 raise(SIGCONT); /* должен быть проигнорирован */ 85 raise(SIGSTOP); /* заснуть, родитель снова.разбудит */ 86 printfC \t > child restarted < —\n" ); 87 exit D2); /* нормальное заверцение, дать возюжность родителю получить значение */ 88 }
378 Глава 10. Сигналы Функция child () обрабатывает поведение порожденного процесса, предпринимая действия для уведомления родителя8. Строка 84 посылает SIGCONT, что может вызвать получение родителем события CLD_CONTINUED. Строка 85 посылает SIGSTOP, который останавливает процесс (сигнал не может быть перехвачен) и вызывает для родителя событие CLD_STOPPED. Когда родитель возобновляет порожденный процесс, последний выводит сообщение, что он снова активен, а затем завершается с известным статусом завершения. 90 /* main установка относящихся к порожденному процессу сведений и сигналов, создание порожденного процесса */ 91 92 int main(int argc, char **argv) 93 { 94 pid_t kid; 95 struct sigaction sa; 96 sigset__t childset, eitiptyset; 97 98 sigemptyset(& emptyset); 99 100 sa.sa_flags = SA_SIGINFO; 101 sa.sa_sigaction = childhandler; 102 sigfillset(& sa.sa_mask); /* при вызове обработчика все заблокировать */ 103 sigaction(SIGCHLD, & sa, NULL); 104 105 sigemptyset(& childset); 106 sigaddset(& childset, SIGCHLD); 107 108 sigprocmask(SIG_.SETMASK, & childset, NULL); /* блокировать его в коде main */ 109 . 110 if ((kid = forkO) == 0) 111 childO; 1.12 113 /* здесь выполняется родитель */ 114 for (;;) { 115 printf("waiting for signals\n"); 116 sigsuspend(& emptyset); 117 } 118 119 return 0; 120 } Программа main() все устанавливает. Строки 100-103 помещают на место обработчик. Строка 100 устанавливает флаг SA__SIGINFO таким образом, что используется обработчик с тремя аргументами. Строки 105-108 блокируют SIGCHLD. Возможно, лучшим именем для функции было бы child_at_school () [ребенок_в_школе]. - Примеч. автора.
10.8. Важные сигналы специального назначения 379 Строка 110 создает порожденный процесс. Строки 113-117 продолжаются в родителе, используя для ожидания входящих сигналов sigsuspend (). 123 /* manage —- разрешение различных событий, которые могут случиться с потомком */ 124 - 125 void manage(siginfo_t *si) 126 { 127 char buf[100]; 128 129 switch (si->si_code) { 130 case CLD_STOPPED: 131 writeA, "\tchild stopped, restarting\n", 27); 132 kill(si->si_pid, SIGCONT); 133 break; 134 13 5 case CLD_CONTINUED: /* not sent on Linux */ 136 writeA, "\tchild continued\n", 17); 137 break; 138 139 case CLDJSXITED: 140 strcpy(buf, "\tchild exited with status "); 141 strcat(buf, format_num(si->si_status)); 142 strcat(buf, "\n"); 143 write(l, buf, strlen(buf)); 144 exit@); /* we're done */ 145 break; 146 147 case CLD_DUMPED: 148 writeA, "\tchild dumpedXn", 14); 149 break; 150 151 case CLD_KILLED: 152 writeA, "\tchild killedNn", 14); 153 break; 154 155 case CLD_TRAPPED: 156 writed, " \tchild trapped\nH, 15); 157 break; 158 }^ 159 } Посредством функции manage () родитель обрабатывает изменение состояния в порожденном процессе, manage () вызывается, когда изменяется состояние и когда порожденный процесс завершился. Строки 130-133 обрабатывают случай, когда потомок остановился; родитель возобновляет его, посылая SIGCONT.
380 Глава 10. Сигналы Строки 135-137 выводят уведомление о возобновлении потомка. Это событие на системах GNU/Linux не происходит, и стандарт POSIX использует в этом случае невыразительный язык, просто говоря, что это событие может появиться, а не появится. Строки 139-145 обрабатывают случай, когда порожденный процесс завершается, выводя статус завершения. Для этой программы родитель также все сделал, поэтому код завершается, хотя в более крупной программе это не то действие, которое должно быть сделано. Другие случаи более специализированные. В случае события CLD__KILLED для получения дополнительных сведений было бы полезным значение status, заполненной функцией waitpid(). Вот что происходит при запуске: $ chlO-status /* Запуск программы */ waiting for signals Entered childhandler /* Вход в обработчик сигнала */ pid 24279 changed status child stopped, restarting /* Обработчик действует */ Exited childhandler waiting for signals ---> child restarted < /* Из потомка */ Entered childhandler reaped process 24279 /* Обработчик родителя опрашивает потомка */ child exited with status 42 К сожалению, поскольку нет способа гарантировать доставку по одному SIGCHLD на каждый процесс, ваша программа должна быть готова восстановить несколько потомков за один проход. 10.9. Сигналы, передающиеся через f ork() и exec () Когда программа вызывает fork(), ситуация с сигналами в порожденном процессе почти идентична ситуации в родительском процессе. Установленные* обработчики остаются на месте, заблокированные сигналы остаются заблокированными и т. д. Однако, любые ожидающие в родителе сигналы в потомке сбрасываются, включая установленный с помощью alarm () временной интервал. Это просто, и это имеет смысл. Когда процесс вызывает одну из функций exec (), положение в новой программе следующее: • Сигналы с установленным действием по умолчанию остаются с этим действием по умолчанию. Все перехваченные сигналы сбрасываются в состояние с действием по умолчанию. • Сигналы, которые игнорируются, продолжают игнорироваться. Особым случаем является SIGCHLD. Если SIGCHLD до вызова exec () игнорировался, он может игнорироваться также и после вызова. В качестве альтернативы для него может быть восстановлено действие по умолчанию. То, что происходит на самом деле, стан-
10.10. Резюме 381 дартом POSIX намеренно не определяется. (Справочные страницы GNU/Linux не определяют, что делает Linux, и поскольку POSIX оставляет это не определенным, любой код, который вы пишете для использования SIGCHLD, должен быть подготовлен для обработки любого случая.) Сигналы, заблокированные до вызова exec (), остаются заблокированными и после вызова. Другими словами, новая программа наследует маску сигналов существующего процесса. Любые ожидающие сигналы (те, которые появились, но были заблокированы) сбрасываются. Новая программа не может их получить. • Временной интервал, остающийся для alarm(), сохраняется на своем месте. (Другими словами, если процесс устанавливает alarm, а затем непосредственно вызывает exec (), новый образ в конечном счете получит SIGALARM. Если он сначала вызывает fork(), родитель сохраняет установки alarm, тогда как потомок, вызывающий exec (), не сохраняет. 13АМЕЧАНИЕ. Многие, если не все, программы предполагают, что сигналы инициализированы действиями по умолчанию и что заблокированных сигналов нет. Таким Iобразом, особенно если не вы писали программу, запускаемую с помощью ехес(), \можно разблокировать перед вызовом exec () все сигналы. 10.10. Резюме «Наша история до настоящего времени, эпизод III» - Арнольд Робби не (Arnold Robbins) - Интерфейсы обработки сигналов развились от простых, но подверженных состояниям гонок, до сложных, но надежных. К сожалению, множественность интерфейсов затрудняет их изучение по сравнению с другими API Linux/Unix. У каждого сигнала есть связанное с ним действие. Действие может быть одним из следующих: игнорирование сигнала; выполнение действия системы по умолчанию или вызов предоставленного пользователем обработчика. Действие системы по умолчанию, в свою очередь, является одним из следующих: игнорирование сигнала; завершение процесса; завершение процесса с созданием его образа; остановка процесса или возобновление процесса, если он остановлен. • signal () и raise () стандартизованы ISO С. signal () управляет действиями для определенных сигналов; raise () посылает сигнал текущему процессу. Остаются ли обработчики сигналов установленными после вызова или сбрасываются для действия по умолчанию, зависит от реализации, signal () и raise () являются простейшими интерфейсами, для многих приложений их достаточно. • POSIX определяет функцию bsd_signal (), которая подобна signal О, но гарантирует, что обработчик остается установленным. • Действия, происходящие после возвращения из обработчика, варьируют в зависимости от системы. Традиционные системы (V7, Solaris, возможно, и другие) восстанавливают действие сигнала по умолчанию. На этих системах прерванный системный вызов возвращает -1, устанавливая в errno значение EINTR. Системы BSD оставляют обработчик установленным и возвращают -1 с errno, содержащим
382 Глава 10. Сигналы EINTR, лишь в случае, когда не было перемещения данных; в противном случае, системный вызов запускается повторно. GNU/Linux придерживается POSIX, который похож, но не идентичен с BSD. Если не было перемещения данных, системный вызов возвращает -1/EINTR. В противном случае он возвращает объем перемещенных данных. Поведение BSD «всегда повторный запуск» доступно через интерфейс sigaction (), но он не является действием по умолчанию. • Обработчики сигналов, используемые с signal (), подвержены состояниям гонок. Внутри обработчиков сигналов должны использоваться исключительно переменные типа volatile sig_atomic_t. (В целях упрощения в некоторых из наших примеров мы не всегда следовали этому правилу.) Таким же образом, для вызова из обработчика сигналов безопасными являются лишь функции из табл. 10.2. Первоначальной попыткой создания надежных сигналов был API сигналов System V Release 3 (скопированный из BSD 4.0). Не используйте его в новом коде. POSIX API содержит множество компонентов: • маску сигналов процесса, перечисляющую текущие заблокированные сигналы; • тип sigset_t для представления масок сигналов, и функции sigf illset (), sigemptyset (), sigaddset (), sigdelset () и sigismember () для работы с ними; • функцию sigprocmask () для установки и получения маски сигналов процесса; • функцию sigpending () для получения набора ожидающих Сигналов; • API sigaction () и struct sigaction во всем их великолепии. Все эти возможности вместе используют блокирование сигналов и маску сигналов процесса для предоставления надежных сигналов. Более того, через различные флаги можно получить повторно запускаемые системные вызовы и более подходящие обработчики сигналов, которые получают большую информацию о причине, вызвавшей определенный сигнал (структура siginf o__t). Механизмами POSIX для посылки сигналов являются kill() и killpgO. Они отличаются от raise () в двух отношениях: A) один процесс может послать сигнал другому процессу или целой группе процессов (конечно, с проверкой прав доступа), и B) посылка сигнала 0 ничего не посылает, но осуществляет проверку. Таким образом, эти функции предоставляют способ проверки наличия определенного процесса или группы процессов и возможность посылки ему (им) сигнала. Сигналы могут использоваться в качестве механизма IPC, хотя такой способ является плохим способом структурирования приложения, подверженным состояниям гонок. Если кто-то держит приставленным к вашей голове ружье, чтобы заставить вас работать таким способом, для правильной работы используйте тщательное блокирование сигналов и интерфейс sigaction (). • SIGALARM и системный вызов alarm () предоставляют низкоуровневый механизм для уведомления о прошествии определенного числа секунд, pause (.) приостанавливает процесс, пока не появятся какие-нибудь сигналы, sleep () использует их для помещения процесса в спящее состояние на заданный период времени: sleep () и alarm () не должны использоваться вместе. Сама pause () создает состояние гонки; вместо этого нужно использовать блокирование сигналов и sigsuspend ().
Упражнения 383 • Сигналы управления заданиями реализуют управление заданиями для оболочки. Большую часть времени следует оставлять их с установленными действиями по умолчанию, но полезно понимать, что иногда имеет смысл их перехватывать. Перехват SIGCHLD позволяет родителю узнать, что делает порожденный им процесс. Использование 'signal (SIGCHLD, SIG_IGN)' (KiiHsigactionO c SA_NOCLDWAIT) вообще игнорирует потомков. Использование sigactionO с SA__NOCLDSTOP предоставляет уведомления лишь о завершении. В последнем случае, независимо от того, заблокирован SIGCHLD или нет, обработчики сигналов для SIGCHLD должны быть готовы немедленно обработать несколько потомков. Наконец, использование sigactionO без SA_NOCLDSTOP с обработчиком сигналов с тремя аргументами дает вам причину получения сигнала. После fork () положение сигналов в порожденном процессе остается тем же самым, за исключением сброса ожидающих сигналов и установленных интервалов таймера. После exec () положение несколько более сложно - в сущности, все, что может быть оставлено, остается; для всего остального восстанавливаются значения по умолчанию. Упражнения 1. Реализуйте bsd__signal () с использованием sigaction (). 2. Если у вас не установлен GNU/Linux, запустите на своей системе chlO-catchint. Является ли ваша система традиционной или BSD? 3. Реализуйте функции System V. Release 3 sighold (), sigrelse (), sigignore (), sigpause () и sigset (), использовав sigaction () и другие подходящие функции из POSIX API. 4. Потренируйте свои навыки в жонглировании битами. В предположении, что сигнал О отсутствует и что имеется не более 31 сигналов, предусмотрите typedef для sigset__t и напишите sigemptyset (), sigf illset (), sigaddset (), sigdelset() и sigismember(). 5. Еще немного потренируйте свои навыки жонглирования битами. Повторите предыдущее упражнение, на этот раз предположив, что наибольшим сигналом является 42. 6. Теперь, когда вы сделали предыдущие два упражнения, найдите sigemptyset() и др. в своем заголовочном файле <signal .h>. (Может потребоваться поискать их; они могут быть в #include файлах, указанных в <signal.h>.) Являются ли они макросами или функциями? 7. В разделе 10.7 «Сигналы для межпроцессного взаимодействия» мы упомянули, что код изделия должен работать с начальной маской сигналов процесса, добавляя и удаляя блокируемые сигналы в вызове sigsuspend(). Перепишите пример, используя для этого соответствующие вызовы. 8. Напишите свою собственную версию команды kill. Интерфейс должен быть таким: kill [-s имя-сигнала] pid ... Если сигнал не указан, программа должна посылать SIGTERM.
384 Глава 10. Сигналы 9. Как вы думаете, почему в современных оболочках, таких, как Bash и ksh93, kill является встроенной крмандой? 10. (Трудное) Реализуйте sleep (), используя alarm(), signal () и pause (). Что случится, если обработчик сигнала для SIGALRM уже установлен? 11. Поэкспериментируйте с chlO-reap. с, изменяя интервал времени, на который засыпает каждый потомок, и организуя достаточное число вызовов sigsuspend() для сбора сведений о всех потомках. 12. Попробуйте заставить chl0-reap2.c испортить информацию в kids, nkids nkidsleft. Теперь добавьте вокруг критического раздела блокирование/разблокирование и посмотрите, есть ли разница.
Глава 11 Права доступа и ID пользователей и групп В этой главе: • 11.1. Проверка прав доступа . . 385 • 11.2. Получение ID пользователя и группы 388 • 11.3. Проверка для действительного пользователя: access () 391 • 11.4. Проверка для эффективного пользователя: euidaccess () (GLIBC) 393 • 11.5. Установка дополнительных битов доступа для каталогов 393 Г 11.6. Установка действительных и эффективных ID 396 • 11.7. Работа со всеми тремя ID: getresuid() и setresuid() (Linux) . . 402 • 11.8. Пересечение минного поля безопасности: setuid root 403 • 11.9. Рекомендуемая литература 404 • 11.10. Резюме 404 • Упражнения 405 J-лпих, вслед за Unix, является многопользовательской системой. В отличие от большинства операционных систем для персональных компьютеров1, в которых имеется лишь один пользователь и в которых, кто бы ни находился перед компьютером, он имеет полный контроль, Linux и Unix различают файлы и процессы по владельцам и группам, которым они принадлежат. В данной главе мы исследуем проверку прав доступа и рассмотрим API для получения и установки идентификаторов владельцев и групп. 11.1. Проверка прав доступа Как мы видели в разделе 5.4.2 «Получение информации о файлах», файловая система хранит идентификаторы владельца и группы файла в виде числовых значений; это типы uid_t и gid__t соответственно. Для краткости мы используем для «идентификатора владельца (пользователя)» и «идентификатора группы» сокращения UID и GID соответственно. У каждого процесса есть несколько связанных с ним идентификаторов пользователя и группы. Для проверки прав доступа в качестве упрощения используется один определенный UID и GID; когда UID процесса совпадает с UID файла, биты прав доступа поль- MacOS X и Windows XP обе являются многопользовательскими системами, но это довольно недавние разработки. - Примеч. автора. 13-159
386 Глава 11. Права доступа и ID пользователей и групп зователя файла диктуют, что может сделать процесс с файлом. Если они не совпадают, система проверяет GID процесса с GID файла; при совпадении используются права доступа группы; в противном случае, используются права доступа для «остальных». Помимо файлов, UID определяет, как один процесс может повлиять на другой путем посылки сигналов. Сигналы описаны в главе 10 «Сигналы». Наконец, особым случаем является суперпользователь, root, root идентифицируется по UID, равным 0. Когда у процесса UID равен 0, ядро позволяет ему делать все, что он захочет: читать, записывать или удалять файлы, посылать сигналы произвольным процессам и т. д. (POSIX в этом отношении более непонятный, ссылаясь на процессы с «соответствующими привилегиями». Этот язык, в свою очередь, просочился в справочные страницы GNU/Linux и справочное руководство GLIBC online Info manual. Некоторые операционные системы действительно разделяют привилегии пользователей, и Linux также движется в этом направлении. Тем не менее, в настоящее время «соответствующие привилегии» означает просто процессы с UID, равным 0.) 11.1.1. Действительные и эффективные ID Номера UID и GID подобны персональным удостоверениям личности. Иногда вам может понадобиться более одного удостоверяющего документа. Например, у вас могут быть водительские права или правительственное удостоверение личности2. Вдобавок, ваш университет или компания могли выдать вам свои удостоверения личности. То же самое относится и к процессам; они имеют при себе множество следующих номеров UID и GID: Действительный ID пользователя UID пользователя, породившего процесс. Эффективный ID пользователя UID, использующийся при большинстве проверок прав доступа. В большинстве случаев эффективный и действительный UID являются одним и тем же. Эффективный UID может отличаться от действительного при запуске, если установлен бит setuid файла исполняемой программы и файл не принадлежит пользователю, запускающему программу. (Вскоре будут дополнительные сведения.) Сохраненный set-user ID Первоначальный эффективный UID при запуске программы (после выполнения exec.) Имеет значение при проверке прав доступа, когда процессу нужно менять действительный и эффективный UID в ходе работы. Эта концепция пришла из System V. Действительный ID группы GID пользователя, создавшего процесс, аналогично действительному UID. Хотя в Соединенных Штатах нет официальных удостоверений личности, во многих странах они имеются. - Примеч. автора.
11.1. Проверка прав доступа 387 Эффективный ID группы GHX использующийся для проверки прав доступа, аналогично эффективному UID. Сохраненный set-group ID Первоначальный эффективный GID при запуске программы, аналогично сохраненному set-user ID. Набор дополнительных групп 4.2 BSD ввело понятие набора групп. Помимо действительного и эффективного GID, у каждого процесса есть некоторый набор дополнительных групп, которым он принадлежит в одно и то же время. Таким образом, когда проверка прав доступа осуществляется для группы файла, ядро проверяет не только эффективный GID, но также и все GID в наборе групп. Каждый процесс может получить все из этих значений. Обычный (не принадлежащий суперпользователю) процесс может переключать свои действительные и эффективные ID пользователя и группы. Процесс root (с эффективным UID, равным 0) может также устанавливать значения таким образом, как ему нужно (хотя это может оказаться односторонней операцией). 11.1.2. Биты Setuid и Setgid Биты setuid и setgid в правах доступа к файлу заставляют процесс принять эффективный UIP или GID, который отличается от действительного. Эти биты накладываются на файл вручную с помощью команды chmod: $ chmod u+s myprogram /* Добавить бит setuid */ $ chmod g+s myprogram /* Добавить бит setgid */ $ Is -1 myprogram -rwsr-sr-x 1 arnold devel 4573 Oct 9 18:17 myprogram Наличие символа s в месте, где обычно находится символ х, указывает на присутствие битов setuid/setgid. Как упоминалось в разделе 8.2.1 «Использование опций монтирования», опция nosuid команды mount для файловой системы предотвращает обращение ядра к битам setuid и setgid. Это мера безопасности; например, пользователь с домашней системой GNU/Linux мог бы вручную изготовить гибкий диск с копией исполняемого файла оболочки с setuid, устанавливающей в root. Но если система GNU/Linux в офисе или лаборатории монтирует файловые системы с гибкими дисками с опцией nosuid, запуск этой оболочки не предоставит доступа с правами root4. Денис Ричи (Dennis Ritchie), создатель С и соавтор Unix, получил патент для бита setuid: Protection of Data File Contents (Защита содержимого фата данных), номер патента США 4135240. См. http : / / www. del phi on. com/details?pn=US04135240 , а также http://www.uspto.gov. AT&T передала патент общественности, разрешив всем использовать свою технологию. - Примеч. автора. Безопасность для систем GNU/Linux и Unix является глубокой темой сама по себе. Это просто пример; см.. раздел 11.9 «Рекомендуемая литература». - Примеч. автора. \%*
388 Глава 11. Права доступа и ID пользователей и групп Каноническим (и возможно, злоупотребляемым) примером программы с setuid является игровая программа. Представьте, что вы написали по-настоящему крутую игру и хотите позволить пользователям системы играть в нее. Игра содержит файл счета, в котором перечислены высшие достижения. Если вы не являетесь системным администратором, вы не можете создать отдельную группу только для тех пользователей, которым разрешено играть в игру и тем самым записывать в файл счета. Но если вы сделаете файл доступным для записи любому, чтобы каждый смог поиграть в игру, тогда каждый сможет также сжульничать и поместить наверх любое имя. Однако, заставив программу устанавливать setuid на вас, пользователи, запускающие игру, получат ваш UID в качестве своего эффективного UID. Игровая программа сможет при этом открывать и обновлять файл счета по мере необходимости, но произвольные пользователи не смогут прийти и отредактировать его. (Вы подвергаете себя также большинству опасностей при программировании setuid; например, если в игровой программе есть дыра, которую можно использовать для запуска оболочки, действующей от вашего имени, все ваши файлы оказываются доступными для удаления или изменения. Это действительно устрашающая мысль.) Та же логика применяется к программам setgid, хотя на практике программы с setgid используются гораздо реже, чем с setuid. (Это также плохо; многие вещи, которые делаются программами с setuid root, легко могут быть сделаны программами с setgid или программами, которые вместо этого устанавливают setuid на обычного пользователя5.) 11.2. Получение ID пользователя и группы Получение от системы сведений о UID и GID просто. Функции следующие: #include <unistd.h> /* POSIX */' uid_t getuid(void); /* Действительный и эффективный UID */ uid_t geteuid(void); gid_t getgid(void); /* Действительный и эффективный GID */ gid__t getegid(void) ; int getgroups(int size, gid_t list[]); /* Список дополнительных групп*/ Функции: uid__t getuid(void) Возвращает действительный UID. uid___t geteuid(void) Возвращает эффективный UID. gid_t getgid(void) 5 Одной из программ, разработанных с этой целью, является GNU userv (ftp://ftp.gnu.org/gnu/ u s erv /). - Примеч. автора.
11.2. Получение ID пользователя и группы 389 Возвращает действительный GID. gid__t getegid(void) Возвращает эффективный GID. int getgroups(int size, gid_t.list[]) Заполняет до size элементов массива list из набора дополнительных групп процесса. Возвращаемое значение является числом заполненных элементов или -1 при ошибке. Включается ли в набор также эффективный GID, зависит от реализации. На системах, совместимых с POSIX, можно передать в size нулевое значение; в этом случае getgroups () возвращает число групп в наборе групп процесса. Затем можно использовать это значение для динамического выделения массива достаточного размера. На не-POSIX системах константа NGOUPS_MAX определяет максимально допустимый размер для массива list. Эту константу можно найти в современных системах в <limi ts . h>, а в старых системах в <sys /param. h>. Вскоре мы представим пример. Возможно, вы заметили, что для получения сохраненных значений set-user ID или set- group ID нет вызовов. Это просто первоначальные значения эффективных UID и GID. Таким образом, для получения шести значений в начале программы вы можете использовать код наподобие этого: uid_t ruid, euid, saved__uid; gid_t rgid, egid, saved_gid; int main(int argc, char **argv) { ruid = getuid(); euid = saved_uid = geteuid(); rgid = getgid(); egid = saved_gid = getegid(); /* ...оставшаяся программа... */ } Вот пример получения набора групп. В качестве расширения gawk предоставляет доступ на уровне awk к значениям действительных и эффективных UID и GID и дополнительному набору групп. Для этого он должен получить набор групп. Следующая функция из main. с в дистрибутиве gawk 3.1.3: 1080 /* init_groupset инициализация набора групп */ 1081 1082 static void 1083 init__groupset () 1084 { 1085 #if defined(HAVE_GETGROUPS) && defined(NGROUPS_MAX) && NGROUPS_MAX > 0 1086 #ifdef GETGROUPS_NOT_STANDARD 1087 /* Для систем, которые не отвечают стандарту, используйте старый способ. */ 1088 ngroups = NGROUPS_MAX;
Ж Глава 1f. Правадоступа иШщшавяшешАшщт 1089.#else ют /* 1031 * Если аба аргумента при вызове равны, О, возвращаемое 1092 * значение явпя&ъся общим шссдом групп. 1093 */ 1094 ngroups =- getgroups @ * NOLDt 1095 #endif 1096 if (ngroups == -1) 1097 fatal (_("could not find groups: %s"), strerror (errno) ) ; 1098 else if (ngroups =0) 1099 return г 1100 1101. /* заполнить группы */ 1102 email ос (graupset, GETGROUPS_T *, ngroups * sizeaf (GETGRQUPS_T) , " init graupset" ); i 1103 1104 ngroups = getgroups(ngroups, graupset}г 1105 if (ngroups == -1) 1X06 fatal (_("could not find groups: %s" ) , strerror (errno) ); r 1107 #endif 120,8 } Переменные ngroups и graupset глобальные; их объявления не показаны. Макрос GETCTGrjPS._T (строжа 1102) является типом да» использования со вторым аргументом; на системе POSIX это gid_tr в противном случае int. Строю* I0S5 и 1WT заключают в скобки все теж* фунжшда; на древних системах, в которых вообще нет наборов групп, тело функщш пустое. Строки 1086^1088 обрабатывают не-POSDC системы; ш ьоомпиляг^ии шросраяаш механизмом комфигуращш определяется GETQRQUPS_K[OT_STMJD^RD.. В этом случае кещ использует NGRQUPS_MA&, как описано выше. (Даже в 2004 г. такие системы все еще существуют и используются; хотя, слава босу* число их уменьшается.) Строки 10&9-1094 для систем POSDC, причем нулевой парамепгр size используется для получения числа групп. Строки 1096-1099 осуществляют проверку ошибок. Если возвращаемое значение О, дополнительных групп нет, поэтому ini t_groupset () просто сразу возвращается. Наконец, строка 1102 для выделения массива достаточного размера использует mallac() (посредством проверяющего ошибки макроса-оболочки, см. ржздел 3.2Л.8 «Пример: чтение строк произвольной длины»). Затем строка 1104 заполняет этот массив.
11.3. Проверка для действительного пользователя: access () 391 11.3. Проверка для действительного пользователя: access() В большинстве случаев значения эффективного и действительного UID и GID являются одними и теми же. Таким образом, не имеет значения, что проверка прав доступа к файлу осуществляется по эффективному ID, а не по действительному. Однако, при написании приложения с setuid или setgid вы можете иногда захотеть проверить, является ли операция, разрешенная для эффективных UID и GID, также разрешенной для действительных UID и GID. В этом заключается задача функции access (): .#include <unistd.h> /* POSIX */ int access (const char *path, int amode); Аргумент path является путем к файлу для проверки действительных UID и GID. amode содержит объединение побитовым ИЛИ одного или нескольких из следующих значений: R__OK Действительный UID/GID разрешает чтение файла. W_OK Действительный UID/GID разрешает запись в файл. Х__ОК Действительный UID/GID разрешает исполнение файла или, в случае каталога, поиск в каталоге. F__OK Проверка существования файла. Проверяется каждый компонент в имени пути, а на некоторых реализациях при проверке для root access () может действовать, как если бы был установлен Х_ОК, даже если в правах доступа к файлу не установлены биты, разрешающие исполнение. (Странно, но верно: в этом случае предупрежденный вооружен.) В Linux нет такой проблемы. Если path является символической ссылкой, access () проверяет файл, на который указывает символическая ссылка. Возвращаемое значение равно 0, если операция для действительных UID и GID разрешена, и -1 в противном случае. Соответственно, если access () возвращает-1, программа с setuid может запретить доступ к файлу, с которым в противном случае эффективный UID/GID смог бы работать: if (access("/some/special/file", R_OK|W_OK) < 0) { fprintf(stderr, "Sorry: /some/special/file: %s\n"/ strerror(errno) ) ; exit(l); > По крайней мере для серии ядра Linux 2.4, когда тест Х__ОК применяется к файловой системе, смонтированной с опцией поехес (см. раздел 8.2.1 «Использование опций монтирования»), тест успешно проходится, если права доступа к файлу имеют разрешение на исполнение. Это верно, несмотря на то, что попытка выполнить файл завершилась бы неудачей.
392 Глава 11. Права доступа и ID пользователей и групп А ЗАМЕЧАНИЕ. . Хотя использование access () перед открытием файла является I обычной практикой, существует состояние гонки: открываемый файл может убыть сброшен при подкачке между проверкой функцией access () и вызовом I open (). Необходимо осмотрительное программирование, такое, как проверка вла- Iдельца и прав доступа с помощью stat () и fstat() до и после вызовов \access () иореп(). Например, программа pathchk проверяет действительность имен путей. GNU версия использует access () для проверки того, что компоненты каталога данного пути действительны. Из Coreutils pathchk .с: 244 /* Возвращает 1, если PATH является годным к использованию 245 каталогом, 0 если нет, 2 если он не существует. */ 246 247 static int 248 dir_ok (const char *path) 249 { 250 struct stat stats; 251 252 if (stat (path, &stats)) Nonzero return = failure 2 53 return 2; 254 255 if (!S__ISDIR (stats.st_mode)) 256 { 257 error @, 0, __(" x%s' is not a directory"), path); 2 58 return 0; 259 } 260 261 /* Используйте access для проверки прав доступа на поиск, 2 62 поскольку при проверке битов' прав доступа st_mode они могут 263 потеряться новыми механизмами управления доступом. Конечно, 264 доступ теряется, если вы используете setuid. */ 265 if (access (path, X_OK) != 0) 266 { 267 if (errno == EACCES) 2 68 error @, 0, __("directory N%s' is not searchable"), path); 269 else 270 error @, errno, "%s", path); 271 return 0; 272 } 273 27 4 return 1; 275 }
11.4. Проверка для эффективного пользователя: euidaccess () (GLIBC) 393 Код прост. Строки 252-253 проверяют, существует ли файл. Если stat () завершится неудачей, файл не существует. Строки 255-259 удостоверяют, что файл в самом деле является каталогом. Комментарий в строках 261-264 объясняет использование accessf). Проверки битов st_mode недостаточно: файл может находиться в файловойПсистеме, которая смонтирована только для чтения, в удаленной файловой системе или в файловой системе, не принадлежащей Linux или Unix, или у файла могут быть атрибуты, предотвращающие доступ. Таким образом, лишь ядро может в действительности сказать, будет ли работать access. Строки 265-272 осуществляют проверку, выдавая сообщение об ошибке, определяемое значением errno (строки 267-270). 11.4. Проверка для эффективного пользователя: euidaccess () (GLIBC) GLIBC предоставляет дополнительную функцию, которая работает подобно access (), но проверяет в соответствии с эффективными UID, GID и набором групп: #include <unistd.h> /* CLIBC */ int euidaccess(const char *path, int amode); Аргументы и возвращаемое значение имеют тот же смысл, как для access (). Когда равны эффективный и действительный UID и эффективный и действительный GID, euidaccess () вызывает для осуществления теста acess (). Это имеет то преимущество, что ядро может проверить файловую систему только для чтения или другие условия, которые не отражаются в правах доступа и владении файлами. В противном случае euidaccess () сравнивает значения владельца и группы файла со значениями эффективных UID и GID и набора групп, используя соответствующие биты прав доступа. Этот тест основан на сведениях о файле от stat (). Если вы пишете переносимую программу, но предпочитаете использовать этот интерфейс, достаточно просто извлечь исходный файл из архива GLIBC и приспособить его для общего использования. 11.5. Установка дополнительных битов доступа для каталогов На современных системах setgid и «липкий» биты имеют особое значение при применении к каталогам. 11.5.1. Группа по умолчанию для новых файлов и каталогов В оригинальной системе Unix, когда open () или creat () создавали новый файл, он получал эффективные UID и GID создавшего их процесса.
394 Глава 11. Права доступа и ID пользователей и групп V7, BSD вплоть до BSD 4.1 и System V вплоть до Release 3 все трактовали каталоги как файлы. Однако, с добавлением дополнительного набора групп в BSD 4.2 способ создания новых каталогов изменился: новые каталоги наследовали группу родительского каталога. Более того, новые файлы также наследовали ID группы родительского каталога, а ие эффективный GID создающего процесса. Идея, лежащая в основе множества групп и каталогов, которые работают таким способом, была в усилении группового взаимодействия. У каждого проекта организации, использующего систему, была бы отдельная назначенная ему группа. Для каждой такой группы в группе этого проекта был бы каталог верхнего уровня, и все файлы проекта имели бы доступ на чтение и запись (а при необходимости и на исполнение). Вдобавок, новые файлы автоматически получают группу родительского каталога. Состоя одновременно в нескольких группах (наборе групп), пользователь мог бы как угодно перемещаться между проектами с помощью простой команды cd, а все файлы и каталоги сохраняли бы свою надлежащую группу. Что происходит на современных системах? Ну, это еще один из немногих случаев, когда можно поймать двух зайцев. SunOS 4.0 придумал механизм, который был включен в System V Release 4; сегодня он используется по крайней мере в Solaris и GNU/Linux. Эти системы придают биту setgid родительского каталога нового файла или каталога следующее значение: Бит setgid родительского каталога сброшен Новые файлы и каталоги получают эффективный GID создающего процесса. Бит setgid родительского каталога установлен Новые файлы и каталоги получают GID родительского каталога. Новые каталоги наследуют также установленный бит setgid. (До SunOS 4.0 бит setgid для каталогов не имел определенного значения.) Следующий сеанс показывает бит setgid в действии: $ cd /txnp /* Перейти в /tmp */ $ Is -Id • /* Проверить его права доступа */ drwxrwxrwt 8 root root 4096 Oct 16 17:40 . $ id /* Отметить текущие группы */ uid=2076(arnold) gid=42(devel) groups=19(floppy),42(devel),2076(arnold) $ mkdir dl ; Is -Id dl /* Создать новый каталог */ drwxr-xr-x 2 arnold devel 4096 Oct 16 17:40 dl /* Эффективный ID группы наследуется */ $ chgrp arnold dl ¦ /* Сменить группу */ ¦ $ chxnod g+s dl /* Добавить бит setgid */ $ Is -Id dl /* Проверить изменение */ drwxr-sr-x 2 arnold arnold 4096 Oct 16 17:40 dl $ cd dl /* Перейти в него */ $ echo this should have group arnold on it > fl /* Создать новый файл */ $ Is -1 ?1 /* Проверить права доступа */'
11.5. Установка дополнительных битов доступа для каталогов 395 -rw-r—г— 1 arnold arnold 36 Oct 16 17:41 fl /* Унаследовано от родителя */ $ inkdir d2 /* Создать каталог */ $ Is -Id d2 /* Проверить права доступа */ drwxr-sr-x 2 arnold arnold 4096 Oct 16 17:51 62 /* Группа и setgid унаследованы */ Файловые системы ext2 и ext3 для GNU/Linux работают указанным способом. Вдобавок они поддерживают специальные опции монтирования grpid и bsdgroups, которые делают «использование группы родительского каталога» семантикой по умолчанию. (Два имени означают одно и то же.) Другими словами, когда используются эти опции монтирования, в родительских каталогах не нужно устанавливать свои биты setgid. Противоположными опциями монтирования являются nogrpidn sysvgroups. Это поведение по умолчанию; однако, бит setgid, если он есть, все равдо учитывается. (Здесь также оба имени означают одно и то же.) POSIX устанавливает, что новые файлы и каталоги наследуют либо эффективный GJD создающего процесса, либо группу родительского каталога. Однако, реализации должны предусмотреть способ заставить новые каталоги наследовать группу родительского каталога. Более того, стандарт рекомендует, чтобы приложения не полагались на то или иное поведение, но в случаях, когда это имеет значение, использовали chown () для принудительного назначения желательного GID для группы нового файла или каталога. 11 -5.2. Каталоги и «липкий» бит «Шерман, установите машину времени для 1976 г.» - М-р Пибоди (Mr, Peabody) - «Липкий» бит ведет начало от версий Unix для PDP-11, он использовался с обычными исполняемыми файлами6. Этот бит использовался с программами, которые предназначались для интенсивного использования, такими, как оболочка и редактор. Когда у программы был установлен этот бит, ядро хранило копию исполняемого кода программы на устройстве подкачки, из которого ее можно было быстро загрузить в память для повторного использования. (Загрузка из файловой системы занимает больше времени: образ на устройстве подкачки хранился в смежных дисковых блоках, тогда как образ в файловой системе мог быть разбросан по всему диску.) Исполняемые образы были «приклet-^ны» к устройству таадцкгчхи, отсщда ж название. Таким образом^ даже если программа в настоящее время не использовалась, предполагалось, что она вскоре могла быть использована другим пользователем, поэтому она могла быть быстро загружена. В современных системах значительно более быстрые дисковое оборудование и память, чем в давнишней PDP-11. Они используют также методику, называемую подкачка по требованию* для загрузки в память лишь тех частей исполняемой программы, которые На ум приходят образы счастливых юных программ, их лица и руки, запачканные шоколадом. - Примеч. автора.
396 Глава 11. Права дхтупа и ID пользователей и групп выполняются. Таким образом, сегодня «липкий» бит обычных исполняемых файлов не служит никаким целям и на самом деле ни на что не влияет. Однако, в разделе 1.1.2 «Каталоги и имена файлов» мы упомянули, что «липкий» бит в каталоге, запись в который в других отношениях разрешена, предотвращает удаление файлов из этого каталога и их переименование любым пользователем, кроме владельца файла или root. Вот пример: $ Is -Id /tmp /* Показать права доступа к /tmp */ drwxrwxrwt 19 root root 4096 Oct 20 14:04 /tmp $ cd /tmp /* Перейти туда */ $ echo this is my file > arnolds-file /* Создать файл */ $ Is -1 arnolds-file /* Показать его права доступа */ -rw-r--r-- 1 arnold devel 16 Oct 20 14:14 arnolds-file $ su - miriam /* Смена пользователя */ Password: $ cd /tmp /* Перейти в /tmp */ $ rm arnolds-file /* Попытка удаления файла */ rm: remove write-protected regular file Narnolds-file' ? у /* rm предупреждает */ rm: cannot remove "arnolds-file1: Operation not permitted /* Дгро запрещает удаление */ Основным назначением этой особенности является как раз использование в таких каталогах, как /tmp, куда хотят помещать свои файлы множество пользователей. С одной стороны, каталог должен иметь права записи для всех, чтобы каждый мог создавать там свои файлы. С другой стороны, раз запись разрешена для всех, любой пользователь может удалять файлы всех остальных пользователей! «Липкий» бит каталога красиво решает эту проблему. Для добавления к файлу или каталогу «липкого» бита используйте 'chmod +t': $ mkdir mytmp /* Создать каталог */ $ chmod a+wxt mytmp /* Добавить права записи для всех и «липкий» бит */ $ Is -Id mytmp /* Проверить результат */ drwxrwxrwt 2 arnold devel 4096 Oct 20 14:23 mytmp В заключение, обратите внимание, что владелец каталога также может удалить файлы, даже если они не принадлежат ему. 11.6. Установка действительных и эффективных ID Все становится интереснее, когда процессу приходится менять значения UID и GID. Установка набора групп проста. Изменение значений действительных и эффективных UID и GID сложнее. 11.6.1. Изменение набора групп Функция setgroups () устанавливает новый набор групп: #include <sys/types.h> /* Common */ #include <unistd.h>
11.6. Установка действительных и эффективных ID 397 #include <grp.h> int setgroups(size^t size, const gid_t *list); Параметр size указывает, сколько элементов в массиве list. Возвращаемое значение равно 0, если все было нормально, и-1 с установленным errno в противном случае. В отличие от функций для манипулирования значениями действительных и эффективных UID и GID, эту функцию может вызвать лишь процесс, действующий как root. Это один пример того, что POSIX называет привилегированной операцией', сама она как таковая не стандартизуется POSIX. set groups () используется любой программой, которая осуществляет регистрацию в системе, такой как /bin/ login для регистрации в консоли и /bin/sshd для удаленной регистрации с помощью ssh. 11.6.2. Изменение действительного и эффективного ID Работа с двумя различными ID пользователей представляет для программиста приложения проблему. Могут быть вещи, которые программе нужно сделать, работая с эффективным UID, а другие вещи - работая с действительным UID. Например, до того, как в системах Unix появилось управление заданиями, многие программы предоставляли переходы в оболочку, т. е. способ запуска команды или интерактивной оболочки из текущей программы. Хорошим примером этого является редактор ed: набор командной строки, начинающейся с Т, запускает оставшуюся часть строки в качестве команды оболочки. Набрав '! sh\ вы получаете интерактивную оболочку. (Это работает до сих пор - попробуйте!) Предположим, описанная ранее гипотетическая игровая программа также предоставляет переход в оболочку: она должна быть запущена от имени действительного пользователя, а не эффективного. В противном случае, редактирование файла счета или многие гораздо худшие вещи становятся для игрока тривиальной задачей! Таким образом, имеется явная потребность в возможности замены эффективного UID действительным UID. Более того, полезна возможность обратного переключения эффективного UID на первоначальный. (В этом причина необходимости наличия сохраненного set-user ID; появляется возможность восстановления первоначальных привилегий, которые были у процесса при его запуске.) Как и для множества Unix API, различные системы решили проблему разными способами, иногда с использованием одного и того же API, но с другой семантикой, а иногда введением другого API. Погружение в исторические подробности годится лишь для создания головной боли, поэтому мы не будем с этим беспокоиться. Вместо этого мы рассмотрим, что предоставляет POSIX и как работает каждый API. Более того, наше обсуждение фокусируется на значениях действительных и эффективных UID; значения GID работают аналогичным образом, поэтому мы не будем хлопотать с повторением подробностей для этих системных вызовов. Функции следующие: #include <sys/types.h> /* POSIX */ #include <unistd.h>
398 Глава 11. Права доступа и Ш пользователей и групп int seteuid(uid_t euid); /* Установка эффективного ID */ int setegid(gid_t egid); int setuid(uid_t uid) ; /* Установка эффективного ID, root устанавливает все */ int setgid(gid_t gid); int setreuid (uid_t ruid, uid_t euid) ; /* Совместимость с BSD, устанавливаются оба */ int setregid(gid_t rgid, gid_t egid); Есть три набора функций. Первые два были созданы POSIX: int seteuid(uid_t euid) Эта функция устанавливает лишь эффективный UID. Обычный пользователь (не root) может установить в качестве ID лишь в значения действительного, эффективного или сохраненного set-user ID. Приложения, которые будут переключать эффективный UID, должны использовать исключительно эту функцию. Процесс с эффективным UID, равным нулю, может установить в качестве эффективного UID любое значение. Поскольку в качестве значения эффективного UID можно установить также сохраненный set-user ID, процесс может восстановить свои привилегии root с помощью другого вызова seteuid (). int setegid(gid_t egid) Эта функция делает для эффективного Ш группы то, что seteuid () делает для эффективного ID пользователя. Следующий набор функций предлагает первоначальный API Unix для изменения действительных и эффективных UID и GID. В модели POSIX эти функции являются тем. что должна использовать программа с setuid-root для постоянного изменения действительного или эффективного UID: int setuid(uid_t uid) Для обычного пользователя эта функция также устанавливает лишь эффективный UID. Как и для seteuid (), значением эффективного UID может быть любое из текущих значений действительного, эффективного или сохраненного set-user ID. Изменение не постоянно; эффективный UID может быть изменен последующим вызовом на другое значение (из того же исходного набора). Однако, для root эта функция устанавливает в данное значение все три значения для действительного, эффективного и сохраненного set-user Ш. Более того, изменение постоянно; прежнее ID нельзя восстановить. (Это имеет смысл: раз изменился сохраненный set-user ID, нет другого Ш для восстановления.) int setgid(gid_t gid) Эта функция делает для эффективного ID группы то же, что setuid () делает для эффективного Ю пользователя. Используется то же разграничение между обычными пользователями и root. ЗАМЕЧАНИЕ. Возможность изменения ID группы зависит от эффективного ID пользователя. Эффективный GID, равный 0У не имеет особых привилегий.
11.6. Установка действительных и эффективных ID 399 Наконец, POSIX представляет для исторической совместимости две функции из BSD 4.2. В новом коде их лучше не использовать. Однако, поскольку вы, вероятно, увидите использующий эти функции старый код, мы их здесь опишем. int setreuid(uid_t ruid, uid_t euid) Устанавливает данные значения в качестве действительного и эффективного UID. Значение -1 для ruid или euid оставляет соответствующие ID без изменения. (Это похоже на chown (); см. раздел 5.5.1 «Смена владельца файла: chown (), f chown () и 1 chown () ».) root может устанавливать в качестве действительного и эффективного ID любое значение. В соответствии с POSIX пользователи, не являющиеся root, могут изменять лишь эффективный ID; то, что случится, если обычный пользователь попытается изменить действительный UID, «не определено». Однако, справочная страница GNU/ Linux setreuidB) разъясняет поведение Linux: в качестве действительного UID может быть установлено значение действительного или эффективного UID, а в качестве эффективного UID может быть значение действительного, эффективного или сохраненного set-user ID. (Для других систем см. справочную страницу setreuid{2).) int setregid(gid_t rgid, gid_t egid) Делает для действительных и эффективных ID групп то же, что setreuid () делает для действительных и эффективных ID пользователя. Используется то же разграничение между обычными пользователями и root. Сохраненный set-user ID в модели BSD не существует, поэтому лежащей в основе setreuid () и setregid() идеей было упростить переключение между действительным и эффективным ID: setreuid(geteuid(), getuidO); /* обмен действительным и эффективным */ Однако, с принятием POSIX модели сохранения set-user ID и функций seteuid () и setegid () функции BSD не следует использовать в новом коде. Даже документация BSD 4.4 помечает эти функции как устаревшие, рекомендуя вместо них seteuid () /setuid() и setegid() /setgidO . 11.6.3. Использование битов setuid и setgid Есть важные случаи, в которых действующая как root программа должна безвозвратно изменить все три значения действительного, эффективного и сохраненного set-user ID на ID обычного пользователя. Наиболее очевидным случаем является программа login, которую вы используете (либо непосредственно, либо удаленно) каждый раз при регистрации в системе GNU/Linux или Unix. Имеется иерархия программ, как очерчено на рис. 11.1. Код для login слишком сложен, чтобы показать здесь, поскольку он имеет дело с рядом задач, не имеющих отношения к текущему обсуждению. Но мы можем очертить шаги, которые происходят во время регистрации, следующим образом:
400 Глава.11. Права доступа и ID пользователей и групп PID1 PID523 ruid: 0 euid: О РЮ 523 mid: 0 euid: О РЮ 523 ruid: 42 euid: 42 Рис. 11.1. От init через getty через login к shell 1. init является самым первым процессом. Его РГО равен 1. Все другие процессы являются его потомками. Ядро вручную создает процесс 1 во время загрузки и запускает в нем init. Он действует с действительным и эффективным UID, равными нулю, т. е. как root. 2. init читает /etc/inittab, который, помимо прочих вещей, сообщает init о том, на каких устройствах он должен запустить процесс getty. Для каждого такого устройства (такого, как консоль, последовательные терминалы или виртуальные консоли в системе GNU/Linux) init порождает новый процесс. Этот новый процесс использует затем exec () для запуска getty (от «get tty» («получить tty», т. е. терминал)). На многих системах GNU/Linux эта команда называется mingetty. Программа открывает устройство, сбрасывает его состояние и выводит приглашение 'login: \ 3. По получении регистрационного имени getty выполняет login. Программа login ищет имя пользователя в файле паролей, запрашивает пароль и проверяет его. Если пароль подходит, процесс login продолжается. 4. login изменяет домашний каталог пользователя, устанавливает начальное окружение, а затем устанавливает начальный набор открытых файлов. Он закрывает дескрипторы файлов, открывает терминал и использует dup () для копирования дескрипторов файла терминала в 0, 1 и 2. Вот откуда происходят дескрипторы уже открытых файлов стандартного ввода, стандартного вывода и стандартной ошибки. -5 «Ц +- 1 Г ' 1 ^ fork () /exec () 1 getty I ... fork () /exec () I getty 1 exec () I . login open () /dup () setgroups()/setgid()/setuid() i exec ()
11.6. Установка действительных и эффективных ID 401 5. Затем login использует setgroups () для установки дополнительного набора групп, setgid () для установки значений действительного, эффективного и сохраненного set-group ID в соответствующее значение группы пользователя, и наконец, setuid () для установки всех трех значений действительного, эффективного и сохраненного set-user ID в соответствующие значения для регистрирующегося пользователя. Обратите внимание, что вызов setuid () должен быть последним для того, чтобы другие два вызова завершились успешно. 6. Наконец, login вызывает зарегистрированную оболочку пользователя. Оболочки в стиле Борна после этого читают файлы /etc/profile и $НОМЕ/ .profile, если они существуют. Затем оболочка выводит приглашение. Обратите внимание, как один процесс меняет свою сущность от системного процесса до процесса пользователя. Каждый потомок init начинается как копия init. Используя exec (), тот же самый процесс выполняет различные задания. Вызвав setuid () для перехода от root к обычному пользователю, процесс в конечном счете поступает непосредственно для работы пользователя. Когда вы выходите из оболочки (посредством CTRL-D или exit), процесс попросту завершается. Затем init возобновляет цикл, порождая новый getty, который выводит новое приглашение 'login:'. ЗАМЕЧАНИЕ. Открытые файлы остаются открытыми и доступными для использования, далее после изменения процессом своих UID или GID. Таким образом, программы с setuid должны заранее открыть все нужные файлы, изменить их ID на ID действительного пользователя и продолжить оставшуюся часть работы без дополнительных привилегий. В табл. 11.1 приведена сводка шести стандартных функций для манипулирования значениями UID и GID. Таблица 11.1. Сводка API для установки действительных и эффективных IDa Функция seteuid() setegid() setuid() setgid() setreuid() setregid() Устанавливает E E Root: R,E,S Другие: Е Root: R,E,S Другие: Е E, может установить R E, может установить R Постоянно Нет Нет Root: да Другие: нет Root: да Другие: нет Нет Нет Обычный пользователь Из R, E, S H3R, E,S H3R, E Из R, Е Из R, Е toR, E Root Любое Любое Любое Любое Любое Любое a. E(EfTective) эффективный ID, R(Real) действительный ID и S(Saved) сохраненный ID. - Примеч. науч. ред.
402 Глава 11. Права доступа и ID пользователей и групп 11.7. Работа со всеми тремя ID: getresuid () и setresuidO (Linux) Linux предоставляет дополнительные системные вызовы, посредством которых вы можете непосредственно работать с действительными, эффективными и сохраненными ID пользователя и группы: #include <sys/types.h> /* Linux */ #include <unistd.h> int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid) ; int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid); int setresuid(uid_t ruid, uid_t euid, uid_t suid); int setresgid(gid_t rgid, gid_t egid, gid_t sgid); Функции следующие: int getresuid(uid_t *ruid, uid__t *euid, uid_t *suid) Получает значения действительного, эффективного и сохраненного set-user ID. Возвращаемое значение 0 в случае успеха и -1 при ошибке, errno указывает проблему. int getresgid(gid_t *rgid, gid__t *egid, gid__t *sgid) Получает значения действительного, эффективного и сохраненного set-group ID. Возвращаемое значение 0 в случае успеха и -1 при ошибке, errno обозначает проблему. int setresuid(uid_t ruid, uid_t euid, uid__t suid) Устанавливает значения действительного, эффективного и сохраненного set-user ID соответственно. Когда значение параметра равно -1, соответствующий UID остается без изменения. Когда процесс действует как root, параметрами могут быть любые произвольные значения. Однако, использование ненулевого значения для euid вызывает постоянную, безвозвратную утерю привилегии root). В противном случае параметры должны быть одним из значений действительного, эффективного или сохраненного set-user ID. int setresgid(gid__t rgid, gid_t egid, gid_t sgid) Устанавливает значения действительного, эффективного и сохраненного set-group ID соответственно. Когда значение параметра равно -1, соответствующий GID остается без изменений. Эта функция аналогична setresuid (). Функции setresuid () и setresgid () особенно ценны, поскольку их семантика ясно определена. Программист точно знает, каким может быть результат их вызова. Более того, вызовы являются операциями типа «все или ничего»: они либо полностью успешны в осуществлении нужного изменения, либо терпят полную неудачу, оставляя текущее состояние как есть. Это повышает надежность, поскольку, опять-таки можно быть точно уверенным в том, что случилось.
11.8. Пересечение минного поля безопасности: setuid root 403 11.8. Пересечение минного поля безопасности: setuid root Настоящие минные поля трудно, но не невозможно перейти. Однако, это не является чем-то, что можно сделать легко, без тренировки или опыта. Точно также написание программ, которые используют setuid root, является трудной задачей. Имеется много, очень много проблем, о которых нужно знать, и почти все может иметь неожиданные последствия в плане безопасности. Такая попытка должна предприниматься очень осторожно. В частности, стоит специально изучить проблемы безопасности Linux/Unix и потратить время на обучение написанию программ setuid root. Если вы сразу нырнете в эту проблему, прочитав лишь эту книгу и ничего более, можно быть уверенным, что ваша система будет взломана, легко и сразу. Маловероятно, что вы или ваши клиенты будут довольны. Вот несколько руководящих принципов: Как можно меньше действуйте в качестве root. Скупо используйте свои чрезвычайные полномочия, лишь тогда, когда это абсолютно необходимо. Соответствующим образом проектируйте свою программу. Разделите программу на составные части таким образом, чтобы все операции root были выполнены заранее, а оставшаяся программа работала в качестве обычного пользователя. При изменении или сбрасывании привилегий используйте setresuid (), если она у вас есть. В противном случае используйте setreuid(), поскольку у этих функций самая чистая семантика. Используйте setuid () , лишь когда вы хотите сделать постоянное изменение. Переходите от root к обычному пользователю в соответствующем порядке: сначала установите набор групп и значения GID, затем значения UID. • Будьте особенно осторожны с fork () и exec (); действительные и эффективные UID при их вызове не изменяются, если вы не измените их явным образом. Рассмотрите использование прав доступа setgid и особой группы для вашего приложения. Если это будет работать, это убережет вас от большой головной боли. • Рассмотрите отказ от наследуемого окружения. Если вам нужно сохранить некоторые переменные окружения, сохраните как можно меньше. Убедитесь в предоставлении подходящих значений для переменных окружения PATH и IFS. Избегайте execlp () и execvp (), которые зависят от значения переменной окружения PATH (хотя это менее проблематично, если вы сами восстанавливаете PATH). Это просто некоторые из множества тактик по пересечению опасной зоны, известной своими подвохами, минами-ловушками и фугасами. Ссылки на другие источники информации см. в следующем разделе.
404 Глава 11. Права доступа и ID пользователей и групп 11.9. Рекомендуемая литература Безопасность Unix (а следовательно, и GNU/Linux) является темой, требующей знаний и опыта для того, чтобы справиться с ней должным образом. В Эпоху Интернета она стала лишь труднее, не проще. 1. Practical UNIX & Internet Security, 3rd edition, by Simson Garfrnkel, Gene Spafford, and Alan Schwartz, O'Reilly & Associates, Sebastopol, CA, USA, 2003. ISBN: 0-596-00323-4. Это стандартная книга по безопасности Unix. 2. Building Secure Software: How to Avoid Security Problems the Right Way, by John Viega and Gary McGraw. Addison-Wesley, Reading, Massachusetts, USA, 2001. ISBN: 0-201-72152-X. Это хорошая книга по написанию безопасного программного обеспечения, она включает проблемы setuid. Предполагается, что вы знакомы с основными API Linux/ Unix; к моменту прочтения данной книги вы должны быть готовы к ее прочтению. 3. "Setuid Demystified," by Hao Chen, David Wagner, and Drew Dean. Proceedings of the 1 lth USENIX Security Symposium, August 5-9, 2002. http://www.cs.berkeley.edu/~daw/papers/ setuid-usenix02.pdf. Гарфинкель, Спаффорд и Шварц (Garfinkel, Spafford, Schwartz) рекомендуют прочтение этого материала «до того, как вы даже подумаете о написании кода, который пытается сохранять и восстанавливать привилегии». Мы всецело согласны с ними. 11.10. Резюме Использование значений ID пользователя и группы (UID и GID) для идентификации файлов и процессов - вот что превращает Linux и Unix в многопользовательские системы. Процессы имеют значения как действительных, так и эффективных UID и GID, а также набор дополнительных групп. Обычно именно эффективный UED определяет, как один процесс может повлиять на другой, и эффективные UID, GID и набор групп проверяются на соответствие с правами доступа к файлу. Пользователи с эффективным UID, равным нулю, известные как root или суперпользователи, могут делать все, что захотят; система не использует для такого пользователя проверку прав доступа. Концепции сохраненных set-user ID и set-group ID пришли из System V и были приняты POSIX с полной поддержкой в GNU/Linux. Наличие этих отдельных значений ID дает возможность легко и безошибочно переключать при необходимости действительные и эффективные UID (и GID). Программы setuid и setgid создают процессы, в которых действительные и эффективные ID различаются. Программы как таковые помечаются дополнительными битами прав доступа к файлу. Биты setuid и setgid должны быть добавлены к файлу после его создания. getuidf) и geteuidO получают значения действительного и эффективного UID соответственно, a getgid() и getegid() получают значения действительного и эффективного GID соответственно, getgroups () получает набор дополнительных групп, а в среде POSIX может запросить у системы, сколько членов содержит набор групп.
Упражнения 405 • Функция access () осуществляет проверку прав доступа к файлу для действительного пользователя, давая возможность программе setuid проверить полномочия реального пользователя. Обратите внимание, что часто проверка возвращаемых stat () сведений может не представить полной картины при условии, что файл может находиться на не родной или сетевой файловой системе. • Функция GLIBC euidaccess () сходна с access (), но осуществляет проверку на основе значений эффективных UID и GID. • «Липкий» бит и бит setgid при использовании с каталогами привносят дополнительную семантику. Когда для каталога установлен бит setgid, новые файлы в этом каталоге наследуют группу этого каталога. Новые каталоги делают то же самое, они также автоматически наследуют установку бита setgid. Без установленного бита setgid новые файлы и каталоги получают эффективный GID создающего их процесса. «Липкий» бит, установленный для каталогов, в которые в других отношениях разрешена запись, ограничивает право на удаление файла владельцем файла, владельцем каталога и root. Набор групп изменяется с помощью setgroups (). Эта функция не стандартизована POSIX, но существует на всех современных системах Unix. Ее может использовать лишь root. Изменение UID и GID довольно сложно. Семантика различных системных вызовов с течением времени изменилась. Новые приложения, которые будут изменять лишь свои эффективные UID/GID, должны использовать seteuid () и setegid (). Приложения, не действующие от имени root, могут также устанавливать свои эффективные ID с помощью setuid () и setgid (). Вызовы setreuid () и setregid () от BSD были предназначены для обмена значениями UID и GID; их использование в новых программах не рекомендуется. • Приложения, действующие как root, могут перманентно заменить значения действительного, эффективного и сохраненного ID с помощью setuid() и setgidf). Одним из таких примеров является login, которая должна превратиться из программы, выполняющейся как root в не непривилегированную зарегистрированную оболочку, выполняющуюся от имени обычного пользователя. Функции Linux setresuid () и setresgid () следует использовать всегда, когда они доступны, поскольку они обеспечивают самое чистое и наиболее надежное поведение. Написание приложений setuid-root не является задачей для новичка. Если вам нужно сделать такую вещь, сначала специально изучите проблемы безопасности. Для этого можно воспользоваться приведенными выше превосходными источниками. Упражнения 1. Напишите простую версию команды id. Ее назначением является отображение в стандартный вывод ID пользователя и группы с указанием имен групп. Когда эффективный и действительный ID различаются, выводятся оба. Например: $ id uid=2076(arnold) gid=42(devel) groups=19(floppy) , 42(devel)/ 2076(arnold) Ее использование: id [ пользователь ] id -G [ -nr ] [пользователь ]
406 Глава 11. Права доступам ID пользователей и групп id -g [ -nr ] {пользователь ] id -u I -nr ] [пользователь ] При указанном пользователе выводятся сведения об этом пользователе; в противном случае id выводит сведения о пользователе, вызвавшем программу. Опции следующие: -G Выводит все значения групп в виде чисел, без имен. -п Выводит лишь имена, без числовых значений. Применяется с значениями пользователя и группы. -д Выводит лишь эффективный GID. - и Вы водит л ишь эффекта вный UID. 2. Напишите простую программу с именем sume и установите setuid на себя. Она должна запрашивать пароль (см. getpassQ)), который в целях данного примера может быть жестко вшит в исходный код программы. Если лицо, запустившее программу, вводит пароль правильно, sume должна выполнить exec оболочки. Попросите другого пользователя помочь вам ее протестировать. 3. Как вы относитесь к тому, чтобы сделать sume доступной для ваших друзей? Для ваших приятелей студентов или сотрудников? Для каждого пользователя на вашей системе?
Глава 12 Общие библиотечные интерфейсы - часть 2 В этой главе: • 12.1. Операторы проверки: assert () 407 • 12.2. Низкоуровневая память: функции meniXXX () 411 • 12.3. Временные файлы 415 • 12.4. Совершение самоубийства: abort () 423 • 12.5. Нелокальные goto 424 • 12.6. Псевдослучайные числа 431 • 12.7. Расширения метасимволов : 439 • 12.8. Регулярные выражения 448 • 12.9. Рекомендуемая литература 456 • 12.10. Резюме 457 • Упражнения 458 м> главе 6, «Общие библиотечные интерфейсы - часть 1», был представлен первый набор API библиотеки общего пользования. В некотором смысле, эти API поддерживают работу с фундаментальными объектами, которыми управляют системы Linux и Unix: время дня, пользователи и группы для файлов, сортировка и поиск. Данная глава более эклектична; функции API, рассмотренные здесь, не особо связаны друг с другом. Однако, все они полезны в повседневном программировании под Linux/ Unix. Наше представление движется от простых, более общих функций API к более сложным и более специализированным. 12.1. Операторы проверки: assert () Оператор проверки (assertion) является утверждением, которое вы делаете о состоянии своей программы в определенный момент времени ее исполнения. Использование операторов проверок для программирования было первоначально разработано Хоаром (C.A.R. НоагеI. Общая идея является частью «верификации программы»: так же, как вы проектируете и разрабатываете программу, вы можете показать, что она правильна, делая Однако, в своей лекции в честь присуждения премии Тьюринга Ассоциации по вычислительной технике в 1981 г. д-р Хоар утверждает, что эту идею выдвинул сам Алан Тьюринг. - Примеч. автора.
408 Глава 12. Общие библиотечные интерфейсы - часть 2 тщательно аргументированные утверждения о проявлениях кода вашей программы. Часто такие утверждения делаются об инвариантах - фактах о состоянии программы, которые, как предполагается, остаются верными на протяжении исполнения куска программы. Операторы проверки особенно полезны для описания двух разновидностей инвариантов: предусловий и постусловий: условий, которые должны быть истинными соответственно перед и после исполнения сегмента кода. Простым примером предусловий и постусловий является линейный поиск: /* lsearch возвратить индекс с данным значением в массиве или -1,' если не найдено */ int lsearch(int *array, size_t size, int value) { size_t i; /* предусловие: array != NULL */ /* предусловие: size > 0 */ for (i =0; i < size; i++) , . if (array[i] == value) return i; /* постусловие: i •== size */ return -1;. ^ } Этот пример определяет условия, используя комментарии. Но не было бы лучше проверить условия с использованием кода? Это является задачей макроса assert (): #include <assert.h> 7* ISO С */ void assert(/* скалярное выражение */); Когда скалярное выражение ложно, макрос assert () выводит диагностическое сообщение и завершает программу (с помощью функции abort (); см. раздел 12.4 «Совершение самоубийства: abort О»). chl2 -assert .с снова предоставляет функцию lsearch (), на этот раз с оператором проверки и функцией main (): 1 /* chl2-assert.с демонстрация операторов проверки */ 2 3 #include <stdio.h> 4 #include <assert.h> 5 ' 6 /* lsearch — возвращает индекс с данным значением в массиве или -1, если не найдено */ 7 8 int lsearch(int *array, size_t size, int value) 9 ( 10 slze_t i; 11 12 assert(array != NULL); 13 assert(size > 0) ; 14 for (i = 0; i < size; i++)
12.1. Операторы проверки: assert () 409 15 if (array[i] == value) 16 return i; 17 18 assertd == size); 19 20 return -1; 21 } 22 • 23 /* main проверить наши условия */ 24 25 int main(void) 26 { 27 #define NELEMS 4 28 static int array[NELEMS] = { 1# 17, 42, 91.}; 29 int index; 30 31 index = lsearch(array, NELEMS, 21); 32 assert(index == -1); 33 34 index = lsearch(array, NELEMS, 17); 35 assert(index ==1); , 36 37 index = lsearch(NULL, NELEMS, 10); /* won't return */ 38 39 printf("index = %d\n", index); 40 41 return 0; 42 } После компиляции и запуска оператор проверки в строке 12 «выстреливает»: $ chl2-assert /* Запуск программы */ chl2-assert: chl2-assert.с:12: lsearch: Assertion xarray != ((void *H)' failed. Aborted (core dumped) . Сообщение от assert () варьирует от системы к системе. Для GLIBC на GNU/Linux сообщение включает имя программы, имя файла с исходным кодом и номер строки, имя функции, а затем текст завершившегося неудачей условия. (В этом случае именованная константа NULL проявляется в виде своего макрорасширения Ч (void *) 0)'.) Сообщение 'Aborted (core dumped)' означает, что chl2-assert создала файл core; т. е. снимок адресного пространства процесса непосредственно перед его завершением2. Этот файл может быть использован впоследствии с отладчиком; см. раздел 15.3 Как упоминалось в разделе 10.2 «Действия сигналов», некоторые дистрибутивы GNU/Linux запрещают создание файлов core. Чтобы снова разрешить их, поместите в свой файл ~/ . profile строку 'ulimit - S - с unl imi t ed'. - Примеч. автора.
410 Глава 12. Общие библиотечные интерфейсы - часть 2 «Основы GDB». Создание файла core является намеренным побочным результатом assert (); предполагается, что произошла решительная ошибка, и вы хотите исследовать процесс с помощью отладчика для ее определения. Вы можете отменить оператор проверки, компилируя свою программу с помощью опции командной строки '-DNDEBUG'. Когда этот макрос определен до включения <assert. h>, макрос assert () расширяется в код, который ничего не делает. Например: $ gcc -DNDEBUG=lchl2-assert.с -о chl2-assert /* Компиляция с -DNDEBUG */ $ chl2-assert /* Запуск */ Segmentation fault (core dumped) /* Что случилось? */ Здесь мы получили настоящий дамп ядра! Мы знаем, что операторы проверки были запрещены; сообщения «failed assertion» нет Что же случилось? Рассмотрите строку 15 lsearch () при вызове из строки 37 main (). В этом случае переменная array равна NULL. Доступ к памяти через указатель NULL является ошибкой. (Технически различные стандарты оставляют «неопределенным» то, что происходит при разыменовывании указателя NULL. Наиболее современные системы делают то же, что и GNU/Linux; они завершают процесс, посылая ему сигнал SIGSEGV; это, в свою очередь, создает дамп ядра. Этот процесс описан в главе 10 «Сигналы». Этот случай поднимает важный момент относительно операторов проверки. Часто программисты ошибочно используют операторы проверки вместо проверки ошибок времени исполнения. В нашем случае тест 'array ! = NULL' должен был быть проверкой времени исполнения: if (array == NULL) return -1; Тест 'size > 0' (строка 13) менее проблематичен; если size равен 0 или меньше 0, цикл никогда не исполнится, и lsearch () (правильно) возвратит -1. (По правде, этот оператор проверки не нужен, поскольку код правильно обрабатывает случай 'size <= 0'.) Логика, стоящая за отменой оператора проверки, заключается в том, что дополнительные проверки могут снизить производительность программы и поэтому должны быть запрещены в заключительной версии программы. Хоар3, однако, сделал такое замечание: «В конце концов, абсурдно делать тщательные проверки безопасности при отладочных запусках, когда к результатам нет никакого доверия, а затем удалять их из финальных версий, когда ошибочный результат может быть дорогим или катастрофическим. Что бы мы подумали об энтузиасте-мореплавателе, который надевает свой спасательный жилет при тренировке на сухой земле и снимает его, как только выходит в море?» С такими мнениями, наша рекомендация заключается во внимательном использовании операторов проверки: во-первых, для любого данного оператора проверки рассмотрите возможность использования вместо него проверки времени исполнения. Во-вторых, 3 Hints On Programming Language Design, C.A.R. Hoare. Stanford University Computer Science Technical Report CS-73-403 (ftp: //reports.stanford.edu/pub/cstr/reports/cs/tr/73/403/CS-TR-73-403.pdf), December, 1973.- Примеч. автора
12.2. Низкоуровневая память: функцию memxxx () 41Т тщательно разместите свой оператор проверки, чтобы не было возражений против их оставления на своем месте даже в финальной версии вашей программы. Наконец, отметим следующее из раздела «Ошибки» епржочной страницы GNU/Lmux asserttt): assert () реализован как макрос; если у проверяемого выражения есть побочные результаты, поведение программы может меняться в зависимости от того, определен ли NDEBUG. Это может создавать гейзенберговские ошибки, которые исчезают при отключении режима отладки. Знаменитый принцип неопределенности Гейзенберга из физики указывает, что чем более точно вы определите скорость частицы, тем менее точно вы определите ее положение, и наоборот. В терминах непрофессионала это означает что простой факт наблюдения частицы влияет на нее. Сходное явление совершается в программировании, не связанном с физикой частиц: действие компилирования программы для отладки или запуска ее в режиме отладки может изменить поведение программы. В частности, первоначальная ошибка может исчезнуть. Такие ошибки в разговоре называют гейзенберговскими. Справочная страница предостерегает нас от использования при вызовах assert () выражений с побочными эффектами: assert(*р++ == '\п'); Здесь побочным эффектом является увеличение указателя р как часть теста. Когда определен NDEBUG, аргумент выражения исчезает из исходного кода; он никогда не исполняется. Это может привести к неожиданной неудаче. Однако, как только при подготовке к отладке запрет на операторы проверки отменяется, все ндашгает снова работать! Такие проблемы трудно отследить. 12.2. Низкоуровневая память: функции memxxx () Несколько функций предоставляют возможность для работы с произвольными блоками памяти низкоуровневые службы. Все их имена начинаются с префикса "mem*: #include <string.h> /* ISO С */ void *memset{void *buf, int val, size_t count); void *memcpy(void *dest, const void *src, size_t count); void *memmove (void *dest, const void *src, size__t count) ; void *memccpy (void *dest, const void *src, int val, size__t count) ; int memcmp(const void *bufl, const void *buf 2, size_t count) ; void *memchr (const void *buf, int val, size_t count) ;. 12.2.1. Заполнение памяти: memset () Функция memset () копирует значение val (интерпретируемое как unsigned char) в первые count байтов буфера buf. Она особенно полезна для обнуления блоков динамической памяти:
412 Глава 12. Общие библиотечные интерфейсы - часть 2 void *p = malloc(count); if (p != NULL) memset(p, 0, count)/ Однако memset () может использоваться с любой разновидностью памяти, не только с динамической. Возвращаемым значением является первый аргумент: buf. 12.2.2. Копирование памяти: memcpy (), memmove () и memccpy () Три функции копируют один блок памяти в другой. Первые две функции отличаются в обработке перекрывающихся областей памяти; третья копирует память, но останавливается при встрече с определенным значением, void *memcpy(void *dest, const void *src, size__t count) Это простейшая функция. Она копирует count байтов из src в dest. Она не обрабатывает перекрывающиеся области памяти. Функция возвращает dest. void *memmove (void *dest, const void *src/ size__t count) Подобно memcpy (), она также копирует count байтов из src в dest. Однако, она обрабатывает перекрывающиеся области памяти. Функция возвращает dest. void *memccpy(void *dest, const void *src, int val, size_t count) Эта копирует байты из src в dest, останавливаясь либо после копирования val в dest, либо после копирования count байтов. Если она находит val, то возвращает указатель на положение в dest сразу за val. В противном случае возвращается NULL. Теперь, в чем проблема с перекрывающейся памятью? Рассмотрим рис. 12.1. struct xyz { ... } data[8]; memcpy( & data[3], data, sizeof(data[0]) * 4); против memmove(& data[3], data, sizeof(data[0]) * 4) ; struct xyz { ... } data[8]; memcpy( & data[3], data, sizeof(data[0]) * 4) ; vs. memmove (& data[3], data, sizeof(data[0]) * 4), Индекс struct xyz struct xyz struct xyz struct xyz struct xyz struct xyz struct [ xyz [ struct I xyz i i Источник Место назначения Рис. 12.1. Перекрывающиеся копии
12.2. Низкоуровневая память: функции memxxx () 413 Целью является скопировать четыре экземпляра struct xyz от data[0] до data[3] в участок от data[3] до data [6]. Здесь проблемой является data[3]; побайтовое копирование с перемещением в памяти из data [ 0 ] затрет data [ 3 ] до того, как он будет безопасно скопирован в data [ б ]! (Может возникнуть также сценарий, когда копирование в памяти в обратном направлении разрушит перекрывающиеся данные.) Функция memcpy () была первоначальной функцией в System V API для копирования блоков памяти; ее поведение для перекрывающихся блоков памяти не была подробно определена тем или иным способом. Для стандарта С 1989 г. комитет почувствовал, что это отсутствие определенности является проблемой; поэтому они придумали memmove (). Для обратной совместимости memcpy () была оставлена, причем поведение для перекрывающейся памяти было специально отмечено как неопределенное, а в качестве процедуры, корректно разрешающей проблемные случаи, была предложена memmove (). Какую из них использовать в своем коде? Для библиотечной функции, которая не знает, какие области памяти ей передаются, следует использовать memmove (). Таким способом вы гарантируете, что не будет проблем с перекрывающимися областями. Для кода приложения, который «знает», что две области не перекрываются, можно безопасно использовать memcpy (). Как для memcpy (), так и для memmove () (как и для strcpy ()) буфер назначения является первым аргументом, а источник - вторым. Чтобы запомнить это, обратите внимание на порядок, который тот же самый, как в операторе присваивания: dest = src; (Справочные страницы во многих системах не помогают, предлагая прототип в виде Void *memcpy(void *bufl, void *buf2, size_t n)' и полагаясь на то, что текст объяснит, что есть что. К счастью, справочная страница GNU/Linux использует более осмысленные имена.) 12.2.3. Сравнение блоков памяти: memcmp () Функция memcmp () сравнивает count байтов из двух произвольных буферов данных. Возвращаемое ею значение подобно strcmp (): отрицательное, нулевое или положительное, если первый буфер меньше, равен или больше второго. Вы можете поинтересоваться: «Почему бы не использовать для такого сравнения strcmp () ?» Разница между двумя функциями в том, что memcmp () не принимает во внимание нулевые байты (завершающий строку *\0\) Таким образом, memcmp () является функцией, которая используется, когда вам нужно сравнить произвольные двоичные данные. Другим преимуществом memcmp () является то, что она быстрее типичной реализации на С: /* memcmp пример реализации на С, НЕ для реального использования */ int memcmp(const void *bufl, const void *buf2, size_t count) { const unsigned char *cpl = ("Const unsigned char *) bufl; const unsigned char *cp2 = (const unsigned char *.) buf 2;
414 Глава 12. Общие библиотечные интерфейсы - часть 2 int diff; while (count-- != 0) { diff = *cpl++ - *cp2++; if (diff != 0) return diff; } return 0; } Скорость может быть достигнута в результате использования специальных инструкций «блочного сравнения памяти»,, которые поддерживают многие архитектуры, или в результате сравнения единицг превышающих байты. (Эта последняя операция коварна, и лучше оставить ее автору библиотеки.) По этим причинам всегда следует использовать вашу библиотечную версию memcmp {) вместо прокручивания своей собственной. Велика вероятность, что автор библиотеки знает машину лучше вас. 12.2.4. Поиск байта с данным значением: memchr () Функция memchr () сходна с функцией strchr (): она возвращает местоположение определенного значения внутри произвольного буфера. Как и в случае memcmp () против strcmp (), основной причиной для использования memchr () является использование произвольных двоичных данных. GNU wc использует memchr {) при подсчете лишь строк и байтов4, и это позволяет wc быть быстрой. Из wc . с в GNU Coreutils: 257 else if ( ! count_chars && ! count_ccomplicated) 258 { 259 /* Использует отдельный цикл при подсчете лишь строк или строк и байтов - 2 60 но не символов или слов. */ 261 while {(bytes_read = safe_read (fd, buf, BUFFER_SIZE) ) > 0) 262 { 2 63 register char *p = buf; 264 2 65 if (bytes_read == SAFE_READ_ERROR) 266 { 267 error @, errno, "%s", file); 2 68 exit__status = 1; 2 69 break; 270 } 271 Cm. wc(l). wc подсчитывает строки, слова и символы. - Примеч. автора.
12.3. Временные файлы 415 272 while ((p = memchr (р, ' \n', (buf + bytes__read) - р))) 273 { 274 ++р; 275 ++lines; 276 } 277 bytes += bytes_read; 278 } 279 } Внешний цикл (строки 261-278) читает блоки данных из входного файла. Внутрейний цикл (строки 272-276) использует memchr () для поиска и подсчета символов конца строки. Сложное выражение ' (buf + bytes_read) -, р' сводится к числу оставшихся байтов между текущим значением р и концом буфера. Комментарии в строках 259-260 нуждаются в некотором объяснении. Вкратце, современные системы могут использовать символы, занимающие более одного байта в памяти и на диске. (Это несколько более подробно обсуждается в разделе 13.4 «Не могли бы вы произнести это для меня по буквам?».) Таким образом, wc должна использовать другой код, если она различает байты и символы: этот код имеет дело со случаем подсчета байтов. 12.3. Временные файлы Временный файл является в точности тем, что звучит в его названии: файл, в котором при исполнении программы хранятся данные, которые больше не нужны после завершения программы, sort читает со стандартного ввода, если в командной строке не указаны файлы или вы используете в качестве имени файла '-'. Тем не менее, sort должна прочесть все свои входные данные, прежде чем сможет вывести отсортированные результаты. (Подумайте об этом немного, и вы увидите, что это так.) Когда читается стандартный ввод, данные должны быть где-то сохранены, прежде чем sort сможет их отсортировать; это отличное применение для временного файла, sort использует временные файлы также для хранения промежуточных результатов сортировки. Удивительно, но имеются пять различных функций для создания временных файлов. Три из них работают посредством создания строк, представляющих (предположительно) уникальные имена файлов. Как мы увидим, их обычнее следует избегать. Оставшиеся две работают путем создания и открытия временного файла; эти функции предпочтительнее для использования. 12.3.1. Создание временных имен файлов (плохо) Имеются три функции, назначением которых является создание имени уникального, не существующего файла. Получив такое имя, вы можете использовать его для создания временного файла. Поскольку имя уникально, вам «гарантируется» исключительное использование файла. Вот объявления функций:
416 Глава 12. Общие библиотечные интерфейсы - часть 2 #include <stdio.h> char *tmpnam(char *s) ; /* ISO С */ char *tempnam(const char *dir, const char *pfx); /* XSI */ char *mktemp(char *template); /* ISO С */ Все эти функции предусматривают различные вариации одной и той же темы: они заполняют или создают буфер с путем уникального временного имени файла. Файл уникален в том, что создаваемое имя не существует во время создания и возвращения его функциями. Функции работают следующим образом: char *tmpnam(char *s) Генерирует уникальное имя файла. Если s не равен NULL, он должен быть размером по крайней мере L__tmpnam байтов, и в него копируется уникальное имя. Если s равен NULL, имя генерируется во внутреннем статическом буфере, который может быть переписан при последующих вызовах. Префикс каталогов в пути будет P_tmpdir. Как P_tmpdir, так и L_tmpnam определены в <stdio. h>. char *tempnam(const char *dir, const char *pfx) Подобно tmpnam () дает вам возможность указать префикс каталогов. Если dir равен NULL, используется P_tmpdir. Аргумент pf х, если он не равен NULL, определяет до пяти символов для использования в качестве начальных символов имени файла. tempnam() выделяет память для имен файлов, которые она генерирует. Возвращенный указатель может впоследствии использоваться с free () (и это следует сделать, если хотите избежать утечек памяти). char *mktemp(char *template) Генерирует уникальные имена файлов на основе шаблона. Последними шестью символами template должны быть 'ХХХХХХ'; эти символы замещаются уникальным суффиксом. IЗАМЕЧАНИЕ. Аргумент template функции mktemp () переписывается. Поэтому I он не должен быть строковой константой. Многие компиляторы, предшествовавшие I стандарту С, помещают строковые константы в сегмент данных вместе с обычны- \ми глобальными переменными. Хотя в исходном коде они определены как константы, I их можно переписать; таким образом, нередко встречался такой код: I /* Код в старом стиле: не используйте его */ I char *tfile = mktemp("/tmp/myprogXXXXXX"); I /* ...использование файла,.. *f \На современных системах такой код, вероятно, завершится неудачей; в наши дни I строковые константы находятся в сегментах памяти только для чтения. Использование этих функций довольно просто. Файл chl2-mktemp. с демонстрирует mktemp (); нетрудно изменить его для использования других функций: 1 /* chl2-mktemp.c демонстрирует простое использование mktempO. 2 Для краткости проверка ошибок опущена */ 3 4 #include <stdio.h> 5 #include <fcntl.h> /* для флагов открытия */
12.3. Временные файлы 417 б #include <limits.h> /* для РАТН__МАХ */ 7 8 int main(void) 9 { 10 static char template[]'= "/tmp/myfileXXXXXX"; 11 char fname[PATH_MAX]; 12 static char mesg[] = 13 "Here's lookin' at you, kid!\n"; /* вместо "hello, world" */ 14 int fd; 15 16 strcpy(fname, template); 17 mktemp(fname); 18 19 /* ОКНО СОСТОЯНИЯ ГОНКИ ОТКРЫВАЕТСЯ */ 20 21 printf("Filename is %s\n", fname); 22 23 /* ОКНО СОСТОЯНИЯ ГОНКИ ТЯНЕТСЯ ДОСЮДА */ 24 2 5 fd = open(fname, 0_CREAT|0_RDWR|0_TRUNC, 0600); 26 write(fd, mesg, strlen(mesg)); 27 close(fd); 28 29 •/* unlink ( fname) ; */ 30 31 return 0; 32 } Переменная template (строка 10) определяет шаблон имени файла; 'ХХХХХХ' будет заменен уникальным значением. Строка 16 копирует шаблон в fname, которая не является константой: ее можно изменить. Строка 18 вызывает mktemp () для генерирования имени файла, а строка 21 выводит ее, так, чтобы мы могли видеть, что это такое. (Вскоре мы объясним комментарии в строках 19 и 23.) Строка 25 открывает файл, создавая его при необходимости. Строка 26 записывает сообщение в mesg, а строка 27 закрывает файл. В программе, в которой файл должен быть удален после завершения работы с ним, строка 29 была бы не закомментирована. (Иногда временный файл не следует удалять; например, если файл после полной записи будет переименован.) Мы закомментировали ее, чтобы можно было запустить эту программу и посмотреть на файл впоследствии. Вот что происходит при запуске программы: $ chl2-mktemp /* Запуск программы */ Filename is /tmp/myfileQES4WA /* Вывод имени файла */ $ cat /tmp/myfileQES4WA Here's lookin' at you, kid! /* Содержит то, что ожидалось */ 14-159
418 Глава 12. Общие библиотечные интерфейсы - часть 2 $ Is -1 /tmp/myfileQES4WA -rw 1 arnold devel $ rm /txnp/xnyfileQES4WA $ chl2-mktemp /'* /* To же с владельцем и доступом */ 28 Sep 18 09:27 /tmp/myfileQES4WA /* Удалить его */ Используется ли повторно то же имя? */ Нет. Это хорошо */ Снова проверить содержание- ? Ч Filename is /tmp/myfileic7xCy /i $ cat /tmp/myfileic7xCy /v Here's lookin' at you, kid! $ Is -1 /tmp/myfileic7xCy /* Снова проверить владельца и доступ */ -rw 1 arnold devel 28 Sep 18 09:28 /tmp/myfileic7xCy Все кажется работающим замечательно, mktemp () возвращает уникальное имя, chl2-mktemp создает файл с нужными правами доступа, и содержание то самое, которое ожидалось. Так в чем же проблема со всеми этими функциями? Исторически mktemp () использовала простой, предсказуемый алгоритм для создания замещающих символов для 'ХХХХХХ' в шаблоне. Более того, интервал между временем, когда генерируется имя файла, и временем, когда создается сам файл, создает состояние гонки. Как? Ну, Linux и Unix являются системами с квантованием времени, методикой, которая разделяет время процессора между всеми исполняющимися процессами. Это означает, что, хотя программа кажется выполняющейся все время, в действительности есть моменты, когда процессы спят, т. е. ожидают выполнения на процессоре. Теперь рассмотрите программу профессора для отслеживания оценок студентов. Как профессор, так и злоумышленный студент в одно и то же время используют сильно нагруженную многопользовательскую систему. Программа профессора использует для создания временных файлов mktemp (), видевший в прошлом, как оценивающая программа создает и удаляет временные файлы, выяснил алгоритм, который использует mktemp (). (В версии GLIBC нет этой проблемы, но не все системы используют GLIBC!) Рис. 12.2 иллюстрирует состояние гонки и то, как студент его использует. Оценивающая программа: mktemp () создает имя Студент: ^ ОС останавливает программу Создает файл с тем же самым именем и лишней ссылкой I Открывает файл, записывает данные Сохраняет оценок Рис. 12.2. Состояние гонки с mktemp ()
12.3. Временные файлы 419 Вот что случилось. 1. Оценивающая программа использует mktemp () для создания имени файла. По возвращении из mktemp () открыто окно состояния гонки (строка 19 в chl2 -mktemp. с). 2. Ядро останавливает оценивающую программу, чтобы могли поработать другие программы в системе. Это происходит до вызова open (). > Пока оценивающая программа остановлена, студент создает файл с тем же самым именем, которое вернула mktemp (). (Помните, выяснить алгоритм было просто.) Студент создает файл с дополнительной ссылкой, так что когда оценивающая программа удаляет файл, он все еще будет доступен для прочтения. 3. Теперь оценивающая программа открывает файл и записывает в него данные. Студент создал файл с правами доступа -rw-rw-rw-, поэтому это не представляет проблему. 4. Когда оценивающая программа завершает работу, она удаляет временный файл. Однако, у студента все еще есть копия. Например, может быть возможность получения прибыли при заблаговременной продаже своим товарищам их оценок. Наш пример упрощенный; помимо простого похищения данных с оценками умный (хотя и аморальный) студент мог бы вместо этого изменить данные. Если бы профессор не проверял результаты работы своей программы дважды, никто бы ничего не узнал. ХЗАМЕЧАНИЕ. Мы не рекомендуем делать что-либо из этого! Если вы студент, не {пытайтесь сделать что-либо подобное. Первое и самое главное, это неэтично. Во- I вторых, вас могут выгнать из школы. В-третьих, ваши профессора, наверное, не I столь наивны, чтобы использовать mktemp () в своих программах. Этот пример \лишь для иллюстрации! По приведенным и другим причинам, все три описанные в данном разделе функции не следует никогда использовать. Они существуют в POSIX и GLIBC лишь для поддержки старых программ, которые были написаны до того, как была осознана опасность этих процедур. С этой целью системы GNU/Linux генерируют во время компоновки сообщение: $ ее ch12-mktemp.с -о ch 12-mktemp /* Компилировать программу */ /tmp/cclXCvD9.o(.text+0x35): In function 'main': : the use of xmktemp' is dangerous, better use xmkstemp' (Мы рассмотрим mkstemp () в следующем подразделе.) Если бы в вашей системе не было mkstemp (), подумайте, как вы могли бы использовать эти интерфейсы для ее эмулирования. (См. также «Упражнения» для главы 12 в конце.) 12.3.2. Создание и открывание временных файлов (хорошо) Есть две функции, не имеющие проблем состояния гонки. Одна из них предназначена для использования с библиотекой <stdio. h>: #include <stdio.h> /* ISO С */ FILE *tmpfile(void); Другая функция для использования с системными вызовами на основе дескрипторов файлов: #include <stdio.h> /* XSI */ int mkstemp(char *template); 14*
420 Глава 12. Общие библиотечные интерфейсы - часть 2 tmp?ile() возвращает значение FILE*, представляющее уникальный открытый временный файл. Файл открывается в режиме "w+b", w+ означает «открыть для чтения и записи, сначала урезав файл», a b означает двоичный, а не текстовый режим. (На системах GNU/Linuxan и Unix нет разницы, но на других системах есть.) Файл автоматический удаляется, когда закрывается указатель файла; нет способа получить имя файла, чтобы сохранить его содержимое. Программа в chl2 - tmpf ile. с демонстрирует tmpf ile (): /* chl2-tmpfile.с демонстрация tmpfile<). Проверка ошибок для краткости опущена */ #include <stdio.h> int main(void) { static char mesg[] = "Here's lookin' at you, kid!"; FILE *fp; char buf[BUFSIZ]; fp = tmpfile(); fprintf(fp, "%s"/ mesg); fflush(fp); rewind(fp); fgets(buf, sizeof buf, fp); printfC'Got back <%s>\n" , buf) ; fclose(fp); return 0; /* /* /* . /* /*' /* /* /* /* заменяет "hello, world" */ Получить временный файл */ Записать в него */ Сбросить на диск */ Перейти в начало */ Прочесть содержимое */ Вывести полученные данные */ Закрыть файл, закончить */ Все сделано */ Возвращенное значение FILE* .не отличается от любого другого FILE*, возвращенного f open (). При запуске получаем ожидавшиеся результаты: $ chl2-tmpfile Got back <Here's lookin1 at you, kid!> Ранее мы видели, что авторы GLDBC рекомендуют использование функции mkstemp (): $ ее chl2-mktemp.c -о chl2-mktemp /* Компилировать программу•*/ /tmp/cclXCvD9.o(.text+0x35): In function "main*: : the use of Nmktemp' is dangerous, better use vmkstemp1 Эта функция похожа на mktemp () в том, что она принимает имя файла, оканчивающееся на 4ХХХХХХ\ и заменяет эти символы уникальным суффиксом для создания уникального имени файла. Однако, она идет на один шаг дальше. Она создает и открывает файл. Файл создается с доступом 0600 (т. е. -rw-- --). Таким образом, доступ к файлу может получить только пользователь, запустивший программу. Более того, и это то, что делает mkstemp () более безопасной, файл создается с флагом 0_EXCL, который гарантирует, что файл не существует, и не дает никому больше открыть файл.
12.3. Временные файлы 421 Возвращаемое значение является дескриптором открытого файла, который может использоваться для чтения и записи. Для удаления файла после завершения работы с ним должно использоваться имя пути, сохраненное теперь в переданном mkstempO буферу. Все это демонстрируется р chl2rinkstemp. с, который является простой модификадие$:с]112-тр?ile. с: и0. qhl2-mkstemp,.c -.-- демонстрирует mkstenip() . Проверка ошибок для краткости опущена */' #include <stdio.h> #include <fcntl.h> /* для флагов открытия */ #include <limits.h> /* для РАТН_МАХ */ int main(void) { static char template[] = "/tmp/myfileXXXXXX"; char fname [ PATHJMAX] ; static char mesg[] = "Here's lookin' at you, kid!\n"; /* заменяет "hello, world" */ int fd; char buf[BUFSIZ]; int n; strcpy(fname, template); /* Копировать шаблон */ fd = mkstemp (fname) ; /* Создать и открыть временный файл */ printf ("Filename is %s\n", fname) ; /* Вывести его для сведений */ write(fd, mesg, strlen(mesg)); /* Что-нибудь записать в файл */ lseek(fd, OL, SEEK_SET); /* Перейти в начало */ n = read(fd, buf, sizeof (buf)); /* Снсва прочесть даннье; НЕ завершается '\0'! */ printf("Got back: %.*s"/ n, buf); /* Вывести его для проверки */ close(fd); /* Закрыть файл */..-. unlink (fname) ; • /* Удалить его *./ return 0; } ¦'¦ - -1 ¦••¦'¦ При запуске получаем ожидавшиеся результаты: $ chl2-mkstemp Filename is /tmp/myfileuXFWIN Got back: Here's lookin' at you, kid! 12.3.3. Использование переменной окружения tmpdir Многие стандартные утилиты обращают внимание на переменную окружения TMPDIR, используя обозначенный в ней каталог в качестве места для помещения временных файлов. Если TMPDIR не установлена, каталогом по умолчанию для временных файлов обычно является /tmp, хотя на многих современных системах есть также и каталог /var/tmp. /tmp обычно очищается от всех файлов и каталогов административными сценариями оболочки при запуске.
422 Глава 12. Общие библиотечные интерфейсы - часть 2 Многие системы GNU/Linux предоставляют каталог /dev/shirt, использующий файловую систему типа tmpfs: $ df Filesystem IK-blocks Used Available Use% Mounted on Л /dev/hda2 6198436 5136020 747544 88% / /dev/hda5 61431520 27720248 30590648 48% /d none 256616 0 256616 0% /dev/shm Тип файловой системы tmpfs предоставляет электронный (RAM) диск: часть памяти, которая используется, как если бы она была диском. Более того, файловая система tmpfs использует механизмы виртуальной памяти ядра Linux для его увеличения сверх фиксированного размера. Если на вашей системе уйма оперативной памяти, этот подход может обеспечить заметное ускорение. Чтобы протестировать производительность, мы начали с файла /usr/share/dict/linux.words, который является отсортированным списком правильно написанных слов, по одному в строке. Затем мы перемешали этот файл, так что он больше не был сортированным, и создали больший файл, содержащий 500 копий спутанной версии файла: $ Is -1 /tmp/randwords.big /* Показать размер */ -rw-r--r-- 1 arnold devel 204652500 Sep 18 16:02 /tnip/randwords.big $ wc -1 /tmp/randwords.big /* Сколько слов? */ 22713 500 /tmp/randwords-.big /* Свыше 22 миллионов! */ Затем мы отсортировали файл, используя сначала каталог /tmp, а затем с TMPDIR, установленным в /dev/shm5: $ time sort /tmp/randwords.big > /dev/null /* Использование реальных файлов */ real ln\32 .566s user lm23.137s sys 0ml.740s $ time TMPDIR=/dev/shm sort /tmp/randwords.big > /dev/null /* Использование электронного диска */ real lm28.257s user lml8.469s sys 0ml.602s Интересно, использование электронного диска было лишь незначительно быстрее, чем использование обычных файлов. (В некоторых дальнейших тестах оно было даже в действительности медленнее!) Мы предполагаем, что в игру вступил буферный кэш ядра (см. раздел 4.6.2 «Создание файлов с помощью creatO»), весьма эффективно ускоряя файловый ввод/вывод6. 5 Такое использование /dev/shm на самом деле является злоупотреблением; он предназначен для использования в реализации разделяемой памяти, а не в качестве электронного диска. Тем не менее, это полезно для иллюстрации нашей мысли. - Примеч. автора. 6 На нашей системе 512 мегабайтов оперативной памяти, что для старых чудаков вроде автора кажется порядочным. Однако цены на память упали, и вполне обычны системы с одним или более гигабайтами оперативной памяти, по крайней мере, для разработчиков программного обеспечения. - Примеч. автора.
12.4. Совершение самоубийства: abort () 423 У электронного диска есть важный недостаток: он ограничен сконфигурированным для вашей системы размером пространства для подкачки7. Когда мы попытались отсортировать файл, содержащий 1000 копий файла с перемешанными словами, место на электронном диске закончилось, тогда как обычный sort завершился благополучно. Использовать TMPDIR для своих программ просто. Мы предлагаем следующую схему: const char template[] = "myprog.XXXXXX"; char *tmpdir, *tfile; size_t count; int fd; if ((tnpdir = getenvCTMPDIR")) == NULL) /* Испальзсвать значение TMPDIR; если имеется */ tmpdir = "/tmp"; /* В противном случае, /tmp по умолчанию */ count = strlen(tmpdir) + strlen(template) + 2; /* Вьыислить раэлер имени файла */ tfile = (char *)• malloc(count); /* Выделить для него память */ if (tfile == NULL) /* Проверка ошибок */ /* восстановить */ sprintf(tfile, "%s/%s", tmpdir, template); /* Создать завершающий шаблон */ fd = mkstemp(tfile); /* Создать и открыть файл */ /* ...использование tempfile через fd... */ close(fd); /* Очистка */ unlink(tfile); free(tfile); В зависимости от потребностей вашего приложения, вы можете захотеть немедленно удалить файл после его открытия, вместо его удаления как части завершающей очистки. 12.4. Совершение самоубийства: abort () Бывают моменты, когда программа просто не может продолжаться. Обычно лучше всего при этом выдать сообщение об ошибке и вызвать exit (). Однако, особенно для ошибок, являющихся проблемами программирования, полезно не только завершиться, но и создать дамп ядра, который охраняет в файле состояние работающей программы для последующего исследования в отладчике. В этом заключается работа функции abort (): #include <stdlib.h> /* ISO С */ void abort(void).; Функция abort () посылает сигнал SIGABRT самому процессу. Это случится, даже если SIGABRT заблокирован или игнорируется. После этого осуществляется обычное для SIGABRT действие, которое заключается в создании дампа ядра. Пространство для подкачки состоит из одного или более выделенных участков диска, используемых для хранения частей исполняющегося процесса, который не находится в настоящее время в памяти. - Примеч. автора.
424 Глава 12. Общие библиотечные интерфейсы - часть 2 Примером abort () в действии является макрос assert (), описанный в начале данной главы. Когда assert () обнаруживает, что его выражение ложно, он выводит сообщение об ошибке, а затем вызывает abort () для создания дампа ядра. В соответствии со стандартом С, осуществляет abort () очистку или нет, зависит от реализации. Под GNU/Linux она выполняет очистку: все потоки <stdio. h> FILE* перед завершением программы закрываются. Обратите, однако, внимание, что для открытых файлов, использующих системные вызовы на основе дескрипторов файлов, ничего не делается. (Если открыты лишь файлы или каналы, ничего не нужно делать. Хотя мы не обсуждали это, дескрипторы файлов используются также для сетевых соединений, и оставление их открытыми является плохой практикой.) 12.5. Нелокальные переходы «Идите прямо в тюрьму. Не проходите GO. He забирайте 200$». - Монополия - Вы, без сомнения, знаете, чем является goto: передачей потока управления на метку где-то в текущей функции. Операторы goto при скупом употреблении могут послужить удобочитаемости и правильности функции. (Например, когда все проверки ошибок используют goto для перехода на метку в конце функции, такую, как clean_up, код с этой меткой проводит очистку [закрывая файлы и т. п.] и возвращается.) При плохом использовании операторы goto могут привести к так называемой «лапше» в коде, логику которого становится невозможно отследить. Оператор goto в языке С ограничен переходом на метку в текущей функции. Многие языки в семействе Алгола, такие, как Паскаль, допускают использование goto для выхода из вложенной функции в предшествующую вызывающую функцию. Однако в С нет способа, в пределах синтаксиса самого языка, перейти в определенную точку другой функции, пусть даже и вызывающей. Такой переход называется нелокальным переходом. Почему полезен нелокальный переход? Рассмотрите интерактивную программу, которая считывает и выполняет программы. Предположим, пользователь запускает длительное задание, разочаровывается или меняет мнение о данном задании и нажимает CTRL-C для генерирования сигнала SIGINT. Когда запускается обработчик сигнала, он может перейти обратно в начало главного цикла чтения и обработки команд. Строковый редактор ed представляет простой пример этого: $ ed -р •> ' sayings /* Запуск ed, х> ' используется как приглашение */ payings: No ^uch file or directory > a /¦* Добавить текст */ Hello, world Don't panic AC /* Сгенерировать SIGINT */ ? /* Сообщение об ошибке '* один размер подходит всем'' */ > 1,$р /* ed возвращается в командную строку */
12.5. Нелокальные переходы 425 Hello, world xl,$p' prints all the lines Don't panic > w /* Сохранить файл'*/ 25 > q /* Все сделано */ Внутри себя ed устанавливает перед циклом команд точку возврата, и обработчик сигнала осуществляет нелокальный переход на эту точку возврата. 12.5s. 1. Использование стандартных функций: set jmp () и long jmp () Нелокальные переходы осуществляются с помощью функций setjmpf) Hlongjmp(). Эти функции используются в двух разновидностях. Традиционные процедуры определены стандартом ISO С: #include <setjmp.h> /* ISO С */ int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val) ; Тип jmp_buf определен через typedef B<setjmp.h>. set jmp () сохраняет текущее «окружение» в env. env обычно является глобальной или статической на уровне файла переменной, так что она может использоваться из вызванной функции. Это окружение включает любую информацию, необходимую для перехода на местоположение, из которого была вызвана set jmp (). Содержание jmp_buf по своей природе машинно- зависимо; таким образом, jmp__buf является непрозрачным типом: тем, что вы используете, не зная, что находится внутри него. set jmp () возвращает 0, когда она вызывается для сохранения в jmp_buf текущего окружения. Ненулевое значение возвращается, когда с использованием окружения осуществляется нелокальный переход: jmp_buf command__loop; /* На глобальном уровне */ /* ... затем в main() ... */ if (setjmp(command_loop) ==0) /* Состояние сохранено, продолжить */ else /* Мы попадаем сюда через нелокальный переход */ printf("?\п");t ed's famous message /* ... теперь начать цикл команд ... */ longjmp () осуществляет переход. Первым параметром является jmp_buf, который должен быть инициализирован с помощью set jmp (). Второй является целым ненулевым значением, которое set jmp () возвращает в первоначальное окружение. Это сделано так, что код, подобный только что показанному, может различить установку окружения и прибытие путем нелокального перехода. Стандарт С утверждает, что даже если longjmp () вызывается со вторым аргументом, равным 0, set jmp () по-прежнему возвращает ненулевое значение. В таком случае она возвращает 1.
426 Глава 12. Общие библиотечные интерфейсы - часть 2 Возможность передать целое значение и вернуться обратно из setjmpO полезна; это позволяет коду уровня пользователя различать причину перехода. Например, gawk использует эту возможность для обработки операторов break и continue внутри циклов. (Язык awk осознанно сделан похожим на С в своем синтаксисе для циклов, с исполь? зованием while, do-while, for, break и continue.) Использование setjmpO выглядит следующим образом (из eval. с в дистрибутиве gawk 3.1.3): 507 case Node_K_while: 508 PUSH_BINDING(loop_tag_stack, loop_tag/ loop_tag_valid); 509 510 stable_tree = tree; . < . 511 while (eval_condition(stable_tree->lnode)) { 512 INCREMENT(stable_tree->exec_count); 513. switch (setjmp(loop_tag)) { 514 case 0: /* обычный не переход */ 515 (void) interpret(stable_tree->rnode); 516 break; 517 case TAG_CONTINUE: /* оператор continue */ 518 break; 519 case TAG_BREAK: /* оператор break */ 520 RESTORE_BINDING(loop_tag_stack, loop_tag, loop_tag_valid); 521 return 1; 522 default: 523 cant_happen(); 524 } 525 } 526 RESTORE_BINDING(loop_tag_stack, loop_tag, loop_tag_valid); 527 break; Этот фрагмент кода представляет цикл while. Строка 508 управляет вложенными циклами посредством стека сохраненных переменных jmp_buf. Строки 511-524 выполняют цикл while (используя цикл С while!). Строка 511 проверяет условие цикла. Если оно истинно, строка 513 выполняет switch на возвращаемое значение set jmp (). Если оно равно 0 (строки 514-516), строка 515 выполняет тело оператора. Однако, когда set jmp () возвращает TAG_BREAK или TAGi_CONTINUE, оператор switch обрабатывает их соответствующим образом (строки 517-518 и 519-521 соответственно). Оператор break на уровне awk передает TAG__BREAK функции longjmpO, a continue уровня awk передает TAG_CONTINUE. Снова из eval. с с некоторыми пропущенными не относящимися к делу подробностями: 657 case Node_K_break: 658 INCREMENT (tree->exec__count) ; /* ... */ 675 longjmp(loop_tag, TAG__BREAK) ; 676 break;
12.5. Нелокальные переходы 427 677 67 8 case Node_K_continue: 679 INCREMENT(tree->exec_bount); 6^96 longjmp (loop__tag, TAG_CONTINUE) ; 697 break; Вы можете думать о set jmp () как об установке метки, а о longjmp () как выполнении goto с дополнительным преимуществом возможности сказать, откуда «пришел» код (по возвращаемому значению). 12.5.2. Обработка масок сигналов: sigset jmp () и siglong jmp () По историческим причинам, которые, скорее всего, утомили бы вас до слез, стандарт С 1999 г. ничего не говорит о влиянии set jmp () и longjmp () на состояние сигналов процесса, a POSIX явно констатирует, что их влияние на маску сигналов процесса * (см. раздел 10.6 «Сигналы POSIX») не определено. Другими словами, если программа изменяет свою маску сигналов процесса между первым вызовом setjmp() и вызовом longjmpf), каково состояние маски сигналов процесса после longjmp () ? Та ли эта маска, когда была впервые вызвана set jmp () ? Или это текущая маска? POSIX явно утверждает, что «нет способа это узнать». Чтобы сделать обработку маски сигналов процесса явной, POSIX ввел две дополнительные функции и один typedef: #include <setjmp.h> /* POSIX */ int sigsetjmp(sigjmp_buf env, int savesigs); /* Обратите внимание: sigjmp_buf, не jmp__buf! */ void siglongjmp(sigjmp_buf env, int val) ; Главным отличием является аргумент savesigs функции sigset jmp (). Если он не равен нулю, текущий набор заблокированных сигналов сохраняется в env вместе с остальным окружением, которое сохраняется функцией set jmp (). siglongjmp () с env, в которой savesigs содержала true, восстанавливает сохраненную маску сигналов процесса. 13АМЕЧАНИЕ. POSIX также ясен в том, что если savesigs равен нулю (false), I сохраняется ли маска сигналов процесса или восстанавливается, не определено, как I в случае с set jmp () /longjmp (). Это, в свою очередь, предполагает, что если I собираетесь использовать 'sigset jmp (env, 0) ', вы такэюе можете не беспоко- I иться: все дело в том, чтобы иметь контроль над сохранением и восстановлением I маски сигналов процесса! 12.5.3. Важные предостережения Есть несколько технических предостережений, о которых нужно знать. Во-первых, поскольку сохранение и восстановление среды может быть беспорядочной машинногзависимой задачей, set jmp () и longjmp () могут быть макросами.
428 Глава 12. Общие библиотечные интерфейсы - часть 2 Во-вторых, стандарт С ограничивает использование set jmp () следующими ситуациями: • В качестве единственного контролирующего выражения в операторе цикла или условном операторе (if, switch)! В качестве одного операнда выражения сравнения (==, < и т. д.), с целой константёй в качестве другого операнда. Выражение сравйенйя может быть единственные контролирующим выражением цикла или условного оператора. °- • В качестве операнда унарного оператора Т, причем результирующее выражение является единственным контролирующим выражением цикла или условного оператора. ••' В качестве всего выражения оператора-выражения, возможно, приведенного к типу void. Например: > (void) setjmp(buf); В-третьих, если вы хотите изменить локальную переменную в функции, которая вызывает setjmp(), после вызова и хотите, чтобы эта переменная сохранила свое последнее присвоенное после long jmp () значение, нужно объявить эту переменную как volatile. В противном случае все локальные переменные, не являющиеся volatile и изменившиеся после того, как была первоначально вызвана set jmp (), имеют неопределенные значения. (Обратите внимание, что сама переменная jmp_buf не должна объявляться как volatile.) Например: I /* chl2-setjmp.с демонстрирует setjmp()/longjmp() и volatile. */ 2 3 #include <stdio.h> 4 #include <setjmp.h> 5 6 jmp_buf env; 7 8 /* comeback выполнение longjmp */ 9 10 void comeback (void)' II { • 12 longjmp(env, 1); - ; 13 printf("This line is never printed\n"); 14 } 15 16 /* main - вызов setjmp, действия с переменными, вывод значений */ 17 18 int main(void) 19 { 20 int i = 5; 21 volatile int j = 6; 22 23 if (setjmp(env) ==0) { /* первый раз */ 24 i++;
12.5. Нелокальные переходы 429 25 j++; 26 printf("first time: i = %d, j = %d\n'\ i, j); 27 comeback(); 2? , } else /* второй раз */ 2^, ,, printf ("second time: i = %d, j = %d\n", i, j) ; /r.', 31 return 0; 32 } . В этом примере сохранение своего значения ко второму вызову printf () гарантируется лишь j (строка 21). Значение i (строка 20) в соответствии со стандартом С 1999 г. не определено. Это может быть 6, может быть 5, а может даже какое-нибудь другое значение! В-четвертых, как описано в разделе 12.5.2 «Обработка масок сигналов: sigsetjmp () и si g long jmp () », стандарт С 1999 г. не делает никаких утверждений о влиянии, еслионо есть, get jmp () и long jmp () на состояние сигналов программы. Если это важно, вамйри- дется вместо них использовать sigsetjmp () и siglongjmp (). В-пятых, эти процедуры содержат поразительные возможности для утечек памяти! Рассмотрим программу, в которой main () вызывает set jmp {), а затем вызывает несколько вложенных функций, каждая из которых выделяет с помощью malloc () динамическую память. Если наиболее глубоко вложенная функция делает long jmp () обратно в main {), указатели на динамическую память теряются. Взгляните на chl2-memleak. с: 1 /* chl2-memleak.c демонстрирует утечки памяти с псмсщью setjmpO /longjmpO . */ 2 3 #include <stdio.h> 4 #include <malloc.h> /* для определения ptrdiff_t в GLIBC */ 5 #include <setjmp.h> 6 #include <unistd.h> 7 8 jmp_buf env; 9 10 void fl(void), f2(void); 11 12 /* main утечка памяти с помощью setjmpO и longjmpO */ 13 14 int main(void) 15 { 16 char *start_break; 17 char *current_break; 18 ptrdiff_t diff; 19 20 start_break = sbrk((ptrdiff_t) 0) ; 21 22 if (setjmp(env) ==0) /* первый раз */ 23 printf("setjmp called\n"); 24 25 current_break = sbrk((ptrdiff_t) 0);
430 Глава 12. Общие библиотечные интерфейсы - часть 2 26 27 diff = current_break - start_break; 28 printf("memsize = %ld\n", (long) diff); 29 30 fl(); 31 ¦-.:•;/>„ 32 return 0; 33 } 34 35 /* fl выделяет память, осуществляет вложенный вызов */ 36 37 void fl(void) 38 { 39 char *p = mallocA024); 40 41 f2(); 42 } 43 44 /* f2 выделяет память, выполняет longjmp */ 45 46 void f2(void) 47 { 4'8 char *p = mallocA024); 49 50 longjmp(env, 1); 51 } Эта программа устанавливает бесконечный цикл, используя set jmp () и longjmp (). Строка 20 использует для нахождения текущего начала кучи sbrk() (см. раздел 3.2.3 «Системные вызовы: brk () и sbrk () »), а затем строка 22 вызывает set jmp (). Строка 25 получает текущее начало кучи; это место каждый раз изменяется, поскольку longjmp () повторно входит в код. Строки 27-28 вычисляют, сколько было выделено памяти, и выводят это количество. Вот что происходит при запуске: $ chl2-memleak /* Запуск программы */ setjmp called memsize =0 memsize = 6372 memsize =6372 memsize =6372 memsize =10468 memsize = 10468 memsize = 14564 memsize = 14564 memsize = 18660 memsize =18660.
12.6. Псевдослучайные числа 431 Память утекает из программы, как через решето. Она работает до тех пор, пока не будет прервана от клавиатуры или пока не закончится память (в этом случае образуется основательный дамп ядра). Каждая из функций f 1 () и f 2 () выделяют память, a f 2 () выполняет long jmp () обратно в main() (строка 51). Когда это происходит, локальные указатели (строки 39 и 48) на выделенную память пропали! Такие утечки памяти может оказаться трудно отследить, поскольку часто выделяются небольшие размеры памяти, и как таковые, они могут оставаться незамеченными в течение ряда лет8. Этот код явно патологический, но он предназначен для иллюстрации нашей мысли: set jmp () и longjmp () могут вести к трудно обнаруживаемым утечкам памяти. Предположим, что f 1 () правильно вызвал free (). Было бы далеко неочевидно, что память никогда не будет освобождена. В более крупной и более реалистичной программе, в которой longjmp () мог быть вызван лишь посредством if, найти такую утечку становится даже еще труднее. Таким образом, при наличии set jmp () и longjmp () динамическая память должна управляться посредством глобальных переменных, а у вас должен быть код, который обнаруживает вход через longjmp () (посредством проверки возвращаемого значения set jmp ()). Такой код должен затем освободить динамически выделенную память, которая больше не нужна. В-шестых, longjmp () и siglongjmp () не следует использовать из функций, зарегистрированных посредством at exit () (см. раздел 9.1.5.3 «Функции завершения»). В-седьмых, set jmp () и longjmp () могут оказаться дорогими операциями на машинах с множеством регистров. При наличии всех этих проблем вы должны строго рассмотреть дизайн своей программы. Если вам не нужно использовать setjmpO и longjmpO, то, может, стоит обойтись без их использования. Однако, если их использование является лучшим способом структурировать свою программу, продолжайте и используйте их, но делайте это осмотрительно. 12.6. Псевдослучайные числа Многим приложениям нужны последовательности случайных чисел. Например, игровые программы, имитирующие бросание костей, раздачу карт или вращение барабанов игровой машины, нуждаются в возможности случайного выбора одного из возможных значений. (Подумайте о программе fortune, содержащей большую коллекцию афоризмов; каждый раз при запуске она «случайно» выдает новое высказывание.) Многие криптографические алгоритмы также требуют наличия случайных чисел «высокого каче- Такая утечка была у нас в gawk. К счастью, она исправлена. - Примеч. автора.
432 Глава 12. Общие библиотечные интерфейсы - часть 2 ства». В данном разделе описываются различные способы получения последовательностей случайных чисел. ХЗЛМЕЧЛНИЕ. Природа случайности, генерация случайных чисел и их «качество» Хявляются обширными темами, выходящими за рамки данной книги. Мы предос- I тавляем введение в доступные функции API, но это все, что мы можем сделать. I Другие источники с более подробной информацией см. в разделе 12.9 «Рекомендуемая литература». Компьютеры по своему строению являются детерминистическими. Одно и то же вычисление с одними и теми же входными данными всегда должно давать одни и те же результаты. Соответственно, они не годятся для генерации истинно случайных чисел, то есть последовательностей чисел, в которых каждое число в последовательности полностью независимо от числа (или чисел), идущих перед ним. Вместо этого разновидности чисел, обычно используемых на программном уровне, называются псевдослучайными числами. То есть в любой данной последовательности номера выглядят независимыми друг от друга, но сама последовательность в целом повторяющаяся. (Эта повторяемость может быть ценным качеством; она обеспечивает детерминизм для программы в целом.) Многие методы предоставления последовательностей псевдослучайных чисел работают посредством осуществления каждый раз одного и того же вычисления с начальным значением (seed). Сохраненное начальное значение затем обновляется для использования в следующий раз. API предоставляет способ указания нового начального значения. Каждое начальное значение дает одну и ту же последовательность псевдослучайных чисел, хотя различные начальные числа дают (должны давать) различные последовательности. 12.6.1. Стандартный С: rand () и srand () Стандартный С определяет две связанные функции для псевдослучайных чисел: #include <stdlib.h> /* ISO С */ int rand(void); void srand(unsigned int seed); rand () каждый раз после вызова возвращает псевдослучайное число в диапазоне от 0 до RAND__MAX (включительно, насколько мы можем судить по стандарту С99). Константа RAND_MAX должна быть по крайней мере 32 767; она может быть больше. srand () дает генератору случайных чисел в качестве начального значения seed. Если srand () никогда не вызывался приложением, rand () ведет себя так, как если бы seed был равен 1. Следующая программа, chl2-rand.c, использует rand() для вывода граней игральных костей. 1 /.* chl2-rand.c генерирует игральные кости, используя rand(). */ 2 3 #include <stdio.h> 4 #include <stdlib.h>
12.6. Псевдослучайные числа б char *die_faces[] = { /* Управляет ASCII графика! */ 7 8 " '* ", /* 1 */ 9 10 11 12 " * * ", /* 2 */ 13 "• 14 15 16 " * * * ", /* 3 */. 17 18 29 " * * 2 0 " ", /* 4 */ 21 » * * 22 23 " *. * 24 * " * ", /* 5 */ 25 " * * 26 27 ••**,* 28 " ", /* 6 */ 29 •• * * * 30 }; 31 32 /* main выводит N различных граней костей */ 33 34 int main(int argc, char **argv) 35 { 3 6 int nfaces; int i, j, k; 37 38 39 40 41 42 43 44 45 46 47 if (argc != 2) { fprintf(stderr, "usage: %s number-die-faces\n", argv[0]) exit(l); } nfaces = atoi(argv[l]); if (nfaces <= 0) { fprintf(stderr, "usage: %s number-die-faces\n", argv[0])
434 Глава 12. Общие библиотечные интерфейсы - часть 2 48 fprintf(stderr, "\tUse a positive number!\n"); 49 exit(l); 50 } 51 52 for (i = 1; i <= nfaces; i++) { >:i 53 j = rand() % 6; /* force to range 0 <= j <= 5 */ 54 printf ("+: + \n" ); 55 for (k = 0; к < 3; k++) \ 56 printf("|%s|\n", die_faces[(j * 3) + k] ) ; 57 printf (" + + \n\nn); 58 } 59 60 return 0; 61 } Эта программа использует простую ASCII-графику для распечатывания подобия грани игральной кости. Вы вызываете ее с числом граней для вывода. Это вычисляется в строке 44 с помощью atoi (). (В общем, atoi () следует избегать в коде изделия, поскольку она не осуществляет проверку на ошибки или переполнение, также как не проверяет вводимые данные.) Ключевой является строка 53, которая преобразует возвращаемое значение rand () в число от нуля до пяти, используя оператор остатка, %. Значение ' j * 3' действует в качестве начального индекса массива die_f aces для трех строк, составляющих каждую грань кости. Строки 55 и 56 выводят саму грань. При запуске появляется вывод наподобие этого: $ chl2-rand 2 /* Вывести две кости */ + + 1 I 1**1 + ._ + + + * * + + Интерфейс rand () восходит еще к V7 и PDP-11. В частности, на многих системах результатом является лишь 16-разрядное число, что значительно ограничивает диапазон чисел, которые могут быть возвращены. Более того, используемый им алгоритм по современным стандартам считается «слабым». (Версия rand () GLIBC не имеет этих проблем, но переносимый код должен быть написан со знанием того, что rand () не является лучшим API для использования.)
12.6. Псевдослучайные числа 435 chl2-rand.c использует для получения значения в определенном интервале простую методику: оператор %. Эта методика использует младшие биты возвращенного значения (как при десятичном делении, когда остаток от деления на 10 или 100 использует одну иЛи две младшие десятичные цифры). Оказывается, исторический генератор rand () производил лучшие случайные значения в средних и старших битах по сравнению с младшими битами. Поэтому, если вы должны использовать rand (), постарайтесь избежать младших битов. Справочная страница GNU/Linux randC) цитирует «Числовые рецепты на С» , которая рекомендует эту методику: j = l+(int)A0.0*rand()/(RAND_MAX+1.0)); /* для числа от 1 до 10 V 12.6.2. Функции POSIX: random() и srandom() BSD 4.3 ввело random () и сопровождающие ее функции. Эти функции используют намного более подходящий генератор случайных чисел, который возвращает 31-разрядное значение. Теперь они входят в расширение XSI, стандартизованное POSIX: #include <stdlib.h> /* XSI */ long random(void); void srandom(unsigned int seed); char *initstate(unsigned int seed, char *state, size__t n); char *setstate(char *state)/ Первые две функции близко соответствуют rand() и srand() и могут использоваться сходным образом. Однако, вместо одного начального значения, которое дает последовательность псевдослучайных чисел, эти функции используют начальное значение вместе с массивом состояния: массивом байтов, который хранит сведения о состоянии для вычисления псевдослучайных чисел. Последние две функции дают возможность управлять массивом состояния. long random(void); Возвращает число в диапазоне от 0 до 231-1. (Хотя справочная страница GNU/Linux randomC) говорит между 0 и RAND_MAX, это верно лишь для систем GLIBC, где RAND_MAX равен 231-1. На других системах RAND_MAX может быть меньше. POSIX явно называет диапазон от 0 до 231-1.) void srandom(unsigned int seed); Устанавливает начальное число. Если srandom () никогда не вызывается, по умолчанию используется 1. char *initstate (unsigned int seed, char *state, size__t n) ; Инициализирует массив state информацией для использования при генерации случайных чисел, seed является начальным значением, как для srandomO, a n является числом байтов в массиве s^te. 9 Numerical Recipes in С: Лге Art of Scientific Computing, 2nd edition, by William H. Press, Brian P. Flannery, Saul A. Teukolsky, and William T. Vetterling. Cambridge University Press, USA, 1993. ISBN: 0-521-43108-5. - Примеч. автора.
436 Глава 12. Общие библиотечные интерфейсы - часть 2 п должен равняться одному из значений 8,32,64,128 или 256. Болыиие,значения дают лучшие последовательности случайных чисел. Значения меньше 8 заставляют random () использовать простой генератор случайных чисел, сходный с rand (). Значения больше 8, не равные одному из значений в списке, округляются до ближайшего подходящего значения. char *setstate(char *state); Устанавливает внутреннее состояние в соответствии с массивом state, который должен быть инициализирован посредством initstate(). Это позволяет переключаться по желанию между различными состояниями, предоставляя множество генераторов случайных чисел. Если initstate () и setstate () никогда не вызывались, random () использует массив внутреннего состояния размером 128. Массив state непрозрачен; вы инициализируете его с помощью initstate () и передается функции random () посредством setstate (), но в другом отношении вам не нужно заглядывать в него. Если вы используете initstate () и setstate!), srandom () вызывать не нужно, поскольку начальное значение включено в информацию 0 состоянии. chl2 -random. с использует эти процедуры вместо rand (). Используется также обычная методика, которая заключается в использовании в качестве начального значения генератора случайных чисел времени дня, добавленного к PID. 1 /* chl2-random.с генерация вращения костей с использованием random() . */ 2 3 #include <stdio.h> 4 tinclude <stdlib.h> 5 #include <sys/types.h> 6 #include <unistd.h> 7 8 char *die_faces[] = { /* Управляет ASCII графика! */ /* ... как раньше ... */ 32 }; 33 34 /* main выводит N различных граней кубиков */ 35 36 int main(int argC/ char **argv) 37 { 3 8 int nfaces; 39 int i, j, k; 40 char state[256]; 41 time_t now; 42 /* ... проверка args, вычисление nfaces, как раньше ... */ 55 56 (void) time(& now); /* В качестве начального значения используется время дня и РП) */
12.6, Псевдослучайные числа 437 57 (void) initstate( (unsigned int) (now <+ getpidO ),. state, s|zeof state); 58.., (void) setstate (state) sr 59; ; ' ¦•-,...¦, . ." , ,../. ": ''"tf...t . . . ,.., ... . ,.' 6fli}<-K for (i =,1; i, <= nfaces; А++); { ,. Л ; , ... . ', .. r 61 j = randomO % 6; /* использовать диапазон 0, <> = j. ,<¦== 5 */ 62 printf("+ + \n" ); 63. for (k = 0; k< 3; k++) 64 printf("|%s|\n"( die_faces[(j * 3) + k] ) ; ' : 65 printf ("+--- —-- + \n\n") ; 66 } 67 68 return 0; 69 } ' Включение PID в состав начального значения гарантирует, что вы получите различные результаты, даже если две программы будут запущены в течение одной и той же секунды. Поскольку она создает последовательности случайных чисел лучшего качества, random () является более предпочтительной по сравнению с rand (), и GNU/Linux и все современные системы Unix ее поддерживают. 12.6.3. Особые файлы /dev/random и /dev/urandom Как rand (), так и srandom () являются генераторами псевдослучайных чисел. Их вывод для одного и того же начального значения является воспроизводимой последовательностью чисел. Некоторым приложениям, подобным криптографическим, необходимо, чтобы их случайные числа были действительно (более) случайными. С этой целью ядро Linux, также как различные BSD и коммерческие Unix системы предусматривают специальные файлы устройств, которые предоставляют доступ к «энтропийному пулу» случайных битов, которые ядро собирает от физических устройств и других источников. Из справочной страницы randomD): /dev/random [Байты, прочитанные из этого файла, находятся] внутри предполагаемого числа шумовых битов в энтропийном пуле, /dev/random должен подходить для использования в случаях, когда необходим высокий уровень случайности, таких, как одноразовая генерация ключа или блока памяти. Когда энтропийный пул пустой, чтение /dev/random будет блокироваться до тех пор, пока не будет собран дополнительный шум окружения. /dev/urandom [Это устройство будет] возвращать столько байтов, сколько затребовано. В результате, если нет достаточной энтропии в энтропийном пуле, возвращаемые значения теоретически уязвимы для криптографической атаки алгоритма, использованного драйвером. Знание того, как это сделать, недоступно в современной не секретной литера-
438 Глава 12. Общие библиотечные интерфейсы - часть 2 туре, но теоретически возможно существование подобной атаки. Если для вашего приложения это представляет проблему, вместо этого используйте /dev/ random. Для большинства приложений чтения из /dev/urandom должно быть вполне достаточно. Если вы собираетесь написать криптографические алгоритмы высокого качества, следует сначала почитать о криптографии и случайности; не полагайтесь здесь на поверхностное представление! Вот еще одна наша программа для бросания костей, использующая /dev /ur andom: 1 /* chl2-devrandam.c генерирует бросание костей, используя /dev/urandam. */ 2 3 #include <stdio.h> 4 #include <fcntl.h> 5 ttinclude <stdlib.h> 6 7 char *die_faces[] = { /* Управляет ASCII графика! */ /* ... как ранее ... */ 31 }; 32 33 /* myrandom возвращает данные из /dev/urandom в виде unsigned long */ 34 3 5 unsigned long myrandom(void) 36 { 37 static int fd = -1; 3 8 unsigned long data; 39 40 if (fd == -1) 41 fd = open ("/dev/urandom", OJRDONLY) ; 42 43 if (fd == -1 || read(fd, & data, sizeof data) <= 0) 44 return random(); /* отступить */ 45 46 return data; 47 } 48 49 /* main вывести N различных граней кубиков */ 50 51 int main(int argc, char **argv)* 52 { 53 int nfaces; 54 int i, j, k; 55 /* ...проверка args, вычисление nfaces, как ранее... */ 68 69 for (i = 1; i <= nfaces; i++) { 7 0 j = myrandom() % 6; /* обеспечить диапазон 0 <= j <= 5 */ 71 printf ("+-. + \n") ; 72 for (k = 0; k < .3; k++)
12.7. Расширения метасимволов 439 73 printf("|%s|\n", die_faces[(j¦* 3) + к] ) ; 74 printf ("+ + \n"):; 7 5 putchar('\n'); 76 } 7Г ¦ ' ' — 7#r? return 0; : - > 79 } Строки 35-47 предоставляют интерфейс вызова функции для /dev/urandom, читая каждый раз данные unsigned long. Издержками является один дескриптор файла, который остается открытым в течение.жизни программы. 12.7. Расширения метасимволов Три набора функции возрастающей сложности предусматривают возможность сопоставления с шаблонами групповых символов оболочки. Многим программам нужны такие библиотечные функции. Одним примером является find: 'find . -name • * . с ' -print'. Другим примером является опция --exclude во многих программах, которая принимает шаблон файлов с групповыми символами для исключения из того или иного действия. В данном разделе по очереди рассматривается каждый набор функций. 12.7.1. Простое сопоставление с шаблоном: f nmatch () Мы начинаем с функции f nmatch () («filename match» - «сопоставление имени файла»): #include <fnmatch.h> /* POSIX */ int fnmatch(const char *pattern, const char *string, int flags); Эта функция сопоставляет string с pattern, который является обычным шаблоном групповых символов оболочки. Значение флагов (которое вскоре будет описано) изменяет поведение функции. Возвращаемое значение равно 0, если string соответствует pattern, FNM_NOMATCH, если не соответствует, и ненулевое значение, если возникла ошибка. К сожалению, POSIX не определяет каких-либо специфических ошибок; соответственно, вы можете лишь сказать, что что-то пошло не так, но не можете сказать, что. Переменная flags является побитовым ИЛИ одного или более флагов, перечисленных в табл. 12.1. Таблица 12.1. Значения флагов для f nmatch () Флаг Только Значение GLIBC FNM_CASEFOLD V Сопоставление с учетом регистра. FNM_FILE_NAME V Синоним GNU для FNM_PATHNAME. FNM_LEADING_DIR V Флаг для внутреннего использования GLIBC; не используйте его в своих программах. Подробности см. BfnmatchC).
440 Глава 12. Общие библиотечные интерфейсы - часть 2 Таблица 12.1. Значения флагов для f ninatch () (Продолжение) Флаг Только Значение GLIBC FNMJNFOESCAPE Обратный слеш является обычным символом, а не знаком перехода. FNM_PATHNAME Слеш в string должен соответствовать слешу в pattern; он не может быть подставлен через *, ? или 4 [. . . ] \ FNM_PERIOD Начальная точка в string подходит, лишь если в pattern также есть начальная точка. Точка должна быть первым символом в string. Однако, если также уетановлен FNM_PATHNAME, точка, которая идет за слешем, также рассматривается как начальная. f nmatch () работает со строками из любого источника; сопоставляемые строки не обязательно должны быть действительными именами файлов. Хотя на практике f nmatch () используется в коде, читающем каталог с помощью readdir () (см. раздел 53.1 «Базовое чтение каталогов»): struct dirent dp; DIR *dir; char pattern[100]; /* ...заполнить шаблон, открыть каталог, проверить ошибки... */ while ((dp = readdir(dir)) != NULL) '{ if (fnmatch(pattern, dir->d_name, FNM_PERIOD) ==0) /* имя файла соответствует шаблону */ else continue; /* не соответствует */ } GNU Is использует f nmatch () для реализации своей опции --ignore. Вы можете предоставить несколько игнорируемых шаблонов (с помощью нескольких опций). Is сопоставляет каждое имя файла со всеми шаблонами. Она делает это с помощью функции file_interesting() в Is.с: 2269 /* Возвращает не ноль, если файл в Nnext' должен быть перечислен. */ 2270 2271 static int 2272 file__interesting (const struct dirent *next) 2273 { 2274 register struct ignore_pattern *ignore; 2275 227 6 for (ignore = ignore__patterns; ignore; ignore = ignore->next) 2277 if (fnmatch (ignore->pattern, next->d_name, FNM_PERI.OD) == 0) 227 8 return 0; 2279
12.7. Расширения метасимволов 441 2280 if (really_all_files 2281 || next->d_name[0] != '.', 2282 j j (all_files 2283 ScSc next->d_name[l] != 'NO' 2284 ScSc (next->d_name[l] != '.' |J nex,t->d_name [2] != '\0'))) 2285 return 1; 2286 2287 return 0; 2288 } Цикл в строках 2276-2278 сопоставляет имя файла со списком шаблонов для игнорируемых файлов. Если один из шаблонов подходит, файл не интересен и f ile_interesting () возвращает false (то есть 0). Переменная all__f iles соответствует опции -А, которая показывает файлы, имена которых начинаются с точки, но не являются '.' и '. . \ Переменная really_all_f iles соответствует опции -а, которая предполагает -А, а также показывает '.' и '. .'. При наличии таких сведений, условие в строках 2280-2284 может быть представлено следующим псевдокодом: if(/*показать все файлы независимо от их имени (-а)*/ OR /* первый символ имени не точка */ OR (/* показать файлы с точкой (-А) */ AND /* в имени файла несколько символов */ AND (/* второй символ не точка */ OR /* третий символ не завершает имя */))) return TRUE; IЗАМЕЧАНИЕ, fnmatchf) может оказаться дорогостоящей функцией, если она I используется в л окали с многобайтным набором символов. Обсудим многобайтные I наборы символов в разделе J 3.4 «Можете произнести это для меня по буквам?». 12.7.2. Раскрытие имени файла: glob () и globf гее () Функции glob () и globf гее () более разработанные, чем f nmatch (): #include <glob.h> /*' POSIX */ int glob(const char *pattern, int flags, int (*errfunc)(const char *epath, int eerrno), glob_t *pglob); void globfree(glob_t *pglob); Функция glob () осуществляет просмотр каталога и сопоставление с шаблонами, возвращая список всех путей, соответствующих pattern. Символы подстановки могут быть включены в нескольких местах пути, а не только в качестве последнего компонента (например, Vusr/*/* . so'). Аргументы следующие: const char *pattern Шаблон для раскрывания.
442 Глава 12. Общие библиотечные интерфейсы - часть 2 int flags Флаги, управляющие поведением glob (), вскоре будут описаны. int (*errfunc)(const char *epath/ int eerrno) Указатель на функцию для использования при сообщениях об ошибках. Это значение может равняться NULL. Если нет и если (*errf unc)() возвращает ненулевое значение или в flags установлен GLOB__ERR, glob () прекращает обработку. Аргументами (*errfunc)() являются путь, вызвавший проблему, и значение errno, установленное функциями opendir (), readdir () или stat (). glob_t *pglob Указатель на структуру glob_t, использующуюся для хранения результатов. Структура glob__t содержит список путей, которые выдает glob (): typedef struct { /* POSIX */ size_t gl_pathc; /* Число найденных подходящих путей.*/ char **gl__pathv; /* Список подходящих путей */ size__t gl_offs; /* Слоты для резервирования в gl_pathv */ } glob_t; size_t gl_pathc Число путей, которые подошли. char **gl__pathv Массив подходящих путей. gl_pathv [ gl_pathc ] всегда равен NULL. size_t gl__offs «Зарезервированные слоты» в gl_pathv. Идея заключается в резервировании слотов спереди от gl_pathv для заполнения их приложением впоследствии, как в случае с именем команды и опциями. Список затем может быть передан непосредственно execvO или execvp () (см. раздел 9.1.4 «Запуск новой программы: семейство exec () >>). Зарезервированные слоты устанавливаются в NULL. Чтобы все это работало, в flags должен быть установлен GLOB_DOOFFS. В табл. 12.2 перечислены стандартные флаги для glob (). Таблица 12.2. Флаги для glob () Флаг Значение GLOB_APPEND Добавить результаты текущего вызова к предыдущим. GLOB_DOOFFS Зарезервировать места gl_offs спереди в gl_pathv. GLOB_MARK Добавлять символ / в конец каждого имени, которое обозначает каталог. GLOB_NOCHECK Если шаблон не соответствует имени какого-нибудь файла, вернуть его без изменений. GLOB_NOESCAPE Рассматривать обратный слеш как обычный символ. Это делает невозможным обозначать метасимволы подстановок. GLOB_NOSORT He сортировать результаты; по умолчанию они сортируются.
12.7. Расширения метасимволов 443 GLIBC версия структуры glob__t содержит дополнительные члены: typedef struct { /* GLIBC */ /* Компоненты POSIX: */ ..,:size_t gl_pathc; /* Число подходящих путей */ char **gl_pathv; /* Список подходящих путей */ size_t gl_offs; /* Резервируемые в gl_pathv слоты */ /* Компоненты GLIBC: */ int gl_flags; /* Копия флагов, дополнительные флаги GLIBC */ void (*gl_closedir)(DIR *); /* Частная версия closedirO */ struct dirent *(*gl_readdir)(DIR *); /* Частная версия readdir() */ DIR *(*gl_opendir)(const char *); /* Частная версия opendir() */ int (*gl_lstat) (const char *, struct stat *); /* Частная версия lstat() */ int (*gl_stat) (const char *, struct stat *) ; /* Частная версия stat () */ } glob_t; Члены структуры следующие: int gl__flags Копия флагов. Включает также GLOB_MAGCHAR, если pattern включал какие-либо метасимволы, void (*gl_closedir)(DIR *) Указатель на альтернативную версию closedir (). struct dirent *(*gl_readdir)(DIR *) Указатель на альтернативную версию readdir (). DIR * (*gl__opendir) (const char *) Указатель на альтернативную версию opendir (). int (*gl__lstat) (const char *, struct stat *) Указатель на альтернативную версию Is tat (). int (*gl__stat) (const char *, struct stat *) Указатель на альтернативную версию stat (). Указатели на альтернативные версии стандартных функций предназначены главным образом для использования в реализации GLIBC; крайне маловероятно, что вы когда-нибудь их используете. Поскольку GLIBC предусматривает поле gl_f lags и дополнительные значения флагов, справочная страница и руководство Info документируют оставшуюся часть структуры GLIBC glob_t. В табл. 12.3 перечислены дополнительные флаги. Флаг GLOB_ONLYDIR действует в качестве подсказки реализации, потому что вызывающий интересуется лишь каталогами. Главным его предназначением является использование другими функциями в GLIBC, а вызывающий по-прежнему должен быть готов обрабатывать файлы, не являющиеся каталогами. Вам не следует использовать этот флаг в своих программах. glob () может быть вызвана более одного раза: при первом вызове флаг GLOB_APPEND не должен быть указан, при всех последующих вызовах он должен быть указан. Вы не можете между вызовами изменять gl_of f s, а если вы изменили какие-
444 Глава 12. Общие библиотечные интерфейсы - часть 2 нибудь значения в gl_pathv или gl_pathc, нужно их восстановить перед последующим вызовом glob (). Таблица 12.3. Дополнительные флаги GLIBG для glob (): ; Флаг Значение Использовать для доступа к каталогам альтернативные функции (см. текст). Выполнить раскрытие фигурных скобок в стиле csh и Bash. Вставить gl_f lags, если были найдены метасимволы. Вернуть шаблон, если он не содержит метасимволов. По возможности сопоставлять лишь каталоги. См. текст. Разрешить соответствие метасимволов наподобие * и ? начальной точке. Выполнить раскрывание тильды в стиле оболочки. Подобно GLOB_TILDE, но если есть проблемы с указанным домашним каталогом, вернуть GLOBJNOMATCH вместо помещения pattern в список. Возможность многократного вызова glob () позволяет накапливать результаты в одном списке. Это довольно практично, приближается к мощным возможностям раскрывания групповых символов оболочки, но на уровне языка программирования С. glob () возвращает 0, если не было проблем, или одно из значений из табл. 12.4, если были. Таблица 12.4. Возвращаемые glob() значения GLOB_ALTDIRFUNC GLOB_BRACE GLOB_MAGCHAR GLOB_NOMAGIC GLOB_ONLYDIR GLOB_PERIOD GLOB_TILDE •GLOB_TILDE CHECK Константа Значение GLOB_ABORTED GLOB_JNFOMATCH GLOB_NOSPACE Просмотр остановлен раньше времени, поскольку был установлен GLOB_ERR или функция (*errf unc)() возвратила ненулевой результат. Ни одно имя файла не соответствовало pattern, а флагСЬОВ_ШСНЕСК не был установлен. Была проблема с выделением динамической памяти. globfreeO освобождает всю память, которую динамически выделила glob(). Следующая программа, chl2-glob.c, демонстрирует glob(): 1 /* chl2-glob.c демонстрирует glob(). */ 2 , х 3 #include <stdio.h> 4 #include <ermo.h> 5 #include <glob.h> 6 7 char *myname;
12.7. Расширения метасимволов 445 8 9 /* globerr выводит сообщение об ошибке для globO */ 10 11 int globerr(const char *path, int eerrno) ~ПГ'Т"" 13 fprintf(stderr, "%s: %s: %s\n", myname, path, strerror(eerrno)) ; 14 return 0; /* let globO keep going */ 15 } 16 17 /* main{) — раскрывает сишолы постановки в ксмащной строке и вьводит результаты */ 18 . 19 int main(int argc, char **argv) 20 { 21 int i; 22 int flags = 0; 23 glob_t results; 24 int ret; 25 26 if (argc ==1) { 27 fprintf(stderr, "usage: %s wildcard ...\h", argv[0]); 28 exit(l); 29 } 30 31 myname = argv[0 J; /* для globerr() */ 32 33 for (i = 1; i < argc; i++) { 34 flags |= (i > 1 ? GLOB_APPEND : 0); 35 ret = glob(argv[i], flags, globerr, & results); 36 if (ret != 0) { 37 fprintf(stderr, "%s: problem with %s (%s), 38 stopping early\n", myname, argv[i], 39 /* опасно: */ (ret == GLOB_ABORTED ? "filesystern problem" : 40 ret == GLOB_NOMATCH ? "no match of pattern" : 41 ret == GLOB_NOSPACE ? "no dynamic memory" : 42 "unknown problem")); 43 break; 44 } 45 } 46 47 for (i = 0; i < results .gl__pathe; i++) 48 printf("%s\n", results.gl_pathv[i]); 49 50 globfree(& results); 51 return 0; 52 }
446 Глава 12. Общие библиотечные интерфейсы - часть 2 Строка 7 определяет irtyname, которая указывает на имя программы; эта переменная для сообщений об ошибках от globerr (), определенной в строках 11-15. Строки 33-45 являются основой программы. Они перебирают в цикле шаблоны, приведенные в командной строке, вызывая для каждого glob () для добавления к списку результат тов. Большую часть цикла составляет обработка ошибок (строки 36-44). Строки 47-48 выводят результирующий список, а строки 50-51 проводят завершающую очистку и возвращаются. Строки 39-41 не являются хорошими; нужно было использовать отдельную функцию, преобразующую целые константы в строки; мы сделали это главным образом ради экономии места. Код наподобие этого может быть сносным для небольших программ, но более крупные должны использовать функцию. Если вы подумаете о работе, происходящей под капотом (открытие и чтение каталогов, сопоставление шаблонов, динамическое выделение памяти для увеличения списка, сортировка списка), можете начать ценить, как много для вас делает glob()! Вот некоторые результаты: $ chl2-glob ¦/usr/lib/x*.so' '../../*.texi• /usr/lib/xchat-autob5.so /usr/lib/xchat-autogb.so . . / . ./00-preface.texi . . / . ./01-intro.texi . ./../02-cmdline.texi ../../03-memory.texi Обратите внимание, что нам пришлось взять аргументы в кавычки, чтобы предотвратить их разворачивание оболочкой! Универсализация имен? Что это? В былые времена, около V6 Unix, для осуществления разворачивания символов подстановки оболочка использовала за кулисами отдельную программу. Эта программа называлась /etc/glob, и согласно исходному коду V6 , имя «glob» было сокращением от «global». Таким образом глагол «to glob» проник в лексикон Unix со значением «осуществлять разворачивание символов подстановки». Это, в свою очередь, дает нам имена функций glob () и globf гее (). Так что обычно недооцениваемое чувство юмора, время от времени проглядывающее из руководства Unix, все еще живо, официально сохраненное в стандарте POSIX. (Можете ли вы представить кого-нибудь в IBM в 70-х или 80-х годах XX века, называющего системную процедуру glob () ?) См. /usr/ source/ si /glob, с в дистрибутиве V6.
12.7. Расширения метасимволов 447 12.7.3. Разворачивание слов оболочкой: wordexp () и wordf гее () Многие члены комитета POSIX чувствовали, что glob() делает недостаточно: им нужна была библиотечная процедура, способная делать все, что может делать оболочка: разворачивание тильды ('echo -arnold'), разворачивание переменных оболочки ('echo $НОМЕ') и подстановку команд ('echo $ (cd ; pwd) ')• Многие другие чувствовали, что glob () не подходила для этой цели. Чтобы «удовлетворить» каждого, POSIX предоставляет две дополнительные функции, которые делают все: #include <wordexp.h> /* POSIX */ int wordexp(const char *words/ wordexp__t *pwordexp, int flags); void wordf г ее (wordexp__t *wordexp); Эти функции работают сходным с glob () и globf гее (¦) образом, но со структурой wordexp_t: typedef struct { size_t we_wordc; /* Число подходящих слов */ char **we_wordv; /* Список развернутых слов */ size_t we_offs; /* Резервируемые в we_wordv'слоты */ } wordexp_t; Члены структуры полностью аналогичны описанным ранее для glob_t; мы не будем здесь повторять все описание. Как и для glob (), поведение wordexp () управляется несколькими флагами. Флаги перечислены в табл. 12.5. Таблица 12.5. Флаги для wordexp () Константа Значение WRDE_APPEND Добавить результаты текущего вызова к предыдущим. WRDE_JDOOFFS Зарезервировать we_offs мест в начале we__wordv. WRDE_NOCMD Запретить подстановку команд. WRDE_REUSE Повторно использовать память, на которую указывает we_wordv. WRDE_SHOWERR He молчать при возникновении во время разворачивания ошибок. WRDE__UNDEF Неопределенные переменные оболочки должны вызывать ошибку. Возвращаемое значение равно 0, если все прошло хорошо, или одно из значений из табл. 12.6, если нет. Мы оставляем вам в качестве упражнения (см. далее) модификацию chl2-glob.c для использования wordexp () и wordf гее (). Вот наша версия в действии: $ chl2-wordexp 'echo $HOME' /* Развертывание переменных оболочки */ echo /home/arnold
448 Глава 12. Общие библиотечные интерфейсы - часть 2 Таблица 12.6. Возвращаемые значения ошибок для wordexp () Константа Значение WRDE_BADCHAR Метасимвол (конец строки, '|', &,;, <, >, (,), {, или }) в недопустимом месте. WRDE_BADVAL Переменная не определена при установленном WRDE_UNDEF. WRDE_CMDSUB Попытка подстановки команды при установленном WRDE_NOCMD. WRDE_NOSPACE Была проблема с выделением динамической памяти. WRDE_SYNTAX Синтаксическая ошибка оболочки. $ chl2-wordexp 'echo $HOME/*.gzl /* Переменные и символы подстановки */ echo /home/arnold/48000.wav.gz /home/arnold/ipmasq-HOWTO.tar.gz /home/arnold/rc.firewall-examples.tar . gz $ chl2-wordexp 'echo ~arnold' /* Развертывание тильды */ echo /home/arnold $ chl2-wordexp 'echo ~arnold/.p*' /* Тильда и символы подстановки */ echo /home/arnold/.postitnotes /home/arnold/.procmailrc /home/arnold/.profile $ chl2-wordexp "echo •«arnold/,p*'" /* Кавычки работают */ echo ~arnold/.p* 12.8. Регулярные выражения Регулярные выражения являются способом описания текстовых шаблонов для сопоставления. Если вы вообще сколько-нибудь использовали GNU/Linux или Unix, вы без сомнения знакомы с регулярными выражениями: они являются фундаментальной частью инструментария программиста Unix. Они неотъемлемы от таких повседневных программ, как grep, egrep, sed, awk, Perl, а также редакторы ed, vi, vim и Emacs. Если вы вообще не знакомы с регулярными выражениями, мы рекомендуем ознакомиться с некоторыми из книг или URL, указанных в разделе 12.9 «Рекомендуемая литература». POSIX определяет два вида регулярных выражений: базовый и расширенный. Программы типа grep, sed и строчный редактор ed используют базовые регулярные выражения. Программы типа egrep и awk используют расширенные регулярные выражения. Следующие функции дают вам возможность использовать в своих программах любой вид: #include <sys/types.h> /* POSIX */ #include <regex.h> int regcomp(regex_t *preg, const char *regex, int cflags);
12.8. Регулярные выражения 449 int regexec(const regex_t *preg, const char *string, size__t nmatch, regmatch__t pmatch[], int eflags); size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size); void regfree(regex_t *preg); Чтобы сопоставить регулярное выражение, нужно сначала откомпилировать строчную версию регулярного выражения. Компиляция преобразует регулярное выражение во внутренний формат. Затем откомпилированная форма исполняется для строки дня проверки, совпадает ли она с первоначальным регулярным выражением. Функции следующие: int regcomp(regex_t *preg, const char *regex, int eflags) Компилирует регулярное выражение regex во внутреннее представление, сохраняя его в структуре regex_t, на которую указывает preg. eflags контролирует процесс компиляции; ее значение равно 0 или побитовому ИЛИ одного или более флагов из табл. 12.7. int regexec(const regex_t *preg, const char *string, size_t nmatch, regmatch__t pmatch[], int eflags) Выполняет откомпилированное регулярное выражение в *preg в строке string. eflags контролирует способ выполнения; ее значение равно 0 или побитовому ИЛИ одного или более флагов из табл. 12.8. Вскоре мы обсудим другие аргументы. size_t regerror(iht errcode, const regex__t *preg, char *errbuf, size_t errbuf_size) Преобразует ошибку, возвращенную regcomp () или regexec (), в удобочитаемую строку. * void regfree (regex__t *preg) Освобождает динамическую память, используемую откомпилированным регулярным выражением в *preg. Заголовочный файл <regex.h> определяет ряд флагов. Некоторые используются с regcomp(); другие используются с regexec (). Однако, все они начинаются с префикса 'REG__\ В табл. 12.7 перечислены флаги для компиляции регулярных выражений с помощью regcomp(). Таблица 12.7. Флаги для regcomp () Константа Значение REG_EXTENDED Использовать расширенные регулярные выражения. По умолчанию используются базовые регулярные выражения. REG_ICASE Сопоставление regexec () игнорирует регистр символов. REG_NEWLINE Операторы, заменяющие любой символ, не включают символ конца строки. REG_NOSUB Информация о начале и конце вложенного шаблона не требуется (см. текст). 15 -159
450 Глава 12. Общие библиотечные интерфейсы - часть 2 Флаги для сопоставления регулярных выражений с помощью regexec () приведены в табл. 12.8. Таблица 12.8. Флаги для regexec () Константа Значение REG_NOTBOL Оператор Л (начало строки) не сопоставляется. REG__NOTEOL Оператор $ (конец строки) не сопоставляется. Флаги REG_NEWLINE, REG_NOTBOL и REG__NOTEOL взаимодействуют друг с другом. Это немного запутано, поэтому мы будем продвигаться небольшими шажками. Когда в с flags не включен REG__NEWLINE, символ конца строки действует в качестве обычного символа. С ним может быть сопоставлен метасимвол '.' (любой символ), а также дополненные списки символов D [А . . . ]'). При этом $ не сопоставляется немедленно с началом вставленного символа новой строки, аЛне сопоставляется немедленно с его концом. • Когда в eflags установлен REG__NOTBOL, оператор А не соответствует началу строки. Это полезно, когда параметр string является адресом символа в середине сопоставляемого текста. Сходным образом, когда в eflags установлен REG_NOTEOL, оператор $ не .соответствует концу строки. Когда в eflags включен REG_NEWLINE, то: • Символ конца строки не соответствует '.' или дополненному списку символов. • Оператор Л всегда соответствует положению непосредственно за вставленным символом конца строки независимо от установки REG_BOL. • Оператор $ всегда соответствует положению непосредственно перед вставленным символом конца строки независимо от установки REG_EOL. Когда вы осуществляете построчный ввод/вывод, как в случае с grep, можно не включать REG__NEWLINE в eflags. Если в буфере несколько строк, и каждую из них нужно рассматривать как отдельную, с сопоставлением А и $, тогда следует включить REG_NEWLINE. Структура regex_t по большей части непрозрачна. Код уровня пользователя может исследовать лишь один член этой структуры; остальное предназначено для внутреннего использования процедурами регулярных выражений: typedef struct { 7* ...здесь внутренний материал... */ size_t re_nsub; . /* ...здесь внутренний материал... */ } regex_t; В структуре regmatch_t есть по крайней мере два члена для использования кодом уровня пользователя: typedef struct { /* ...здесь возможный внутренний материал... */
12.8. Регулярные выражения 451 regoff_t rm_so; /* Смещение начала вложенной строки в байтах */ regoff_t rm_eo; /* Смещение первого байта после вложенной строки */ /* ...здесь возможный внутренний материал... */ } regmatch_t; Как поле re_nsub, так и структура regmatch_t предназначены для сопоставления вложенных выражений. Рассмотрим такое регулярное выражение: [[:пробел:]]+([[:цифра:]]+)[[:пробел:]]+([[:буква:]])+ Каждое из двух вложенных выражений в скобках могут соответствовать одному или более символам. Более того, текст, соответствующий каждому вложенному выражению, может начинаться и заканчиваться в произвольных участках строки. regcomp () устанавливает в поле re__nsub число вложенных выражений в скобках внутри регулярного выражения, regexec () заполняет массив pmatch структур regmatch__t смещениями начальных и конечных байтов текста, соответствующих этим вложенным выражениям. Вместе эти данные позволяют заменять текст - удалять его или заменять другим текстом, точно так же, как в текстовом редакторе. pmatch [ 0 ] описывает часть строки, соответствующую всему регулярному выражению. Участок от pmatchfl] до pmatch[preg->re_nsub] описывает ту часть, которая соответствует каждому вложенному выражению в скобках. (Таким образом, вложенные выражения нумеруются начиная с 1.) Элементы rm_so и rm_eo не используемых элементов массива pmatch установлены в -1. regexec () заполняет не более nmatch-1 элементов pmatch; поэтому следует убедиться, что имеется по крайней мере на 1 элемент больше, чем в preg->re_nsub. Наконец, флаг REG_NOSUB для regcomp () означает, что начальная и завершающая информация не нужна. Этот флаг следует использовать, когда эти сведения не нужны; это потенциально может довольно значительно повысить производительность regexec (). Другими словами, если все, что вам нужно знать, это «соответствует ли?», включите REG_NOSUB. Однако, если нужно также знать, «где находится соответствующий текст?», этот флаг следует опустить. В заключение, как regcomp (), так и regexec () возвращают 0, если они успешны, или определенный код ошибки, если нет. Коды ошибок перечислены в табл. 12.9. Таблица 12.9. Коды ошибок regcomp () и regexec () Константа Значение REG_BADBR Содержимое ' \ {. . . \}' недействительно. REG_BADPAT Регулярное выражение недействительно. REG_BADRPT Символу ?, + или * не предшествует действительное регулярное выражение. REG_EBRACE Фигурные скобки ('\ {. . . \}') не сбалансированы. REG_EBRACK Квадратные скобки ('[...]') не сбалансированы. 15*
452 Глава 12. Общие (библиотечные интерфейсы - часть 2 Таблица 12.9. Коды ошибок regcomp () и regexec () (Продолжение) Константа Значение REG_ECOLLATE В шаблоне использован недействительный элемент сортировки. REG__ECTYPE В шаблоне использован недействительный класс символов. REG_EESCAPE В шаблоне есть завершающий символ \ REG_JEPAREN Группирующие скобки ('(...)' или *\ (... . \)') не сбалансированы. REG__ERANGE Конечная точка в диапазоне не действительна. REG_.ESРАСЕ Функции не хватило памяти. REG_ESUBREG Цифра в 4\дифра'недействительна. REG_NOMATCH Строка не соответствует шаблону. Для демонстрации регулярных выражений chl2-grep.c предусматривает базовую реализацию стандартной программы grep, которая отыскивает соответствие шаблону. Наша версия использует по умолчанию базовые регулярные выражения. Для использования вместо этого расширенных регулярных выражений она принимает опцию -Е, а для игнорирования регистра симролов опцию -i. Как и настоящая grep, если в командной строке не указаны файлы, наша grep читает со стандартного ввода, а для обозначения стандартного ввода, как и в настоящей grep, может быть использовано имя файла '-'. (Эта методика полезна для поиска в стандартном вводе наряду с другими файлами.) Вот программа: I /* chl2-grep.c - Простая версия grep, использующая функции POSIX */ 2 3 #define _GNU_SOURCE 1 /* для getlineO */ 4 #include <stdio.h> 5 #include <errno.h> 6 #include <regex.h> 7 #include <unistd.h> 8 #include <sys/types.h> 9 10 char *myname; /* для сообщений об ошибках */ II int ignore_case =0; /* опция -i: игнорировать регистр */ 12 int extended =0; /* опция -Е: использовать расширенные регулярные выражения */ 13 int errors =0; /* число ошибок */ 14 15 regex_t pattern; /* шаблон для поиска */ 16 17 void compile_pattern(const char *pat); 18 void process (const char *naiue/ FILE *fp) ; 19 void usage(void);
12.8. Регулярные выражения 453 Строки 10-15 объявляют глобальные переменные программы. Первый набор (строки 10—13) для опций и сообщений об ошибках. Строка 15 объявляет pat tern, в кб^рой хранится откомпилированный шаблон. Строки 17-19 объявляют другие функциипрограммы. 21 /* main обработка опций, открывание файлов */ 22 23 int main(int argc, char **argv) 24 { 25 int c; 26 int i; 27 FILE *fp; 28 29 myname = argv[0]; 30 while ((c = getopt(argc, argv, ":iE")) != -1) { 31 switch (c) { 32 case 'i': 33 ignore_case = 1; 34 break; , 3 5 case 'E': 36 extended = 1; 37 break; 38 case ¦.'?': 39. usage(); 40 break; 41 } 42 } 43 44 if (optind == argc) /* проверка исправности */ 45 usage(); 46 47 compile_pattern(argv[optind]); /* компилировать шаблон */ 48 if (errors) /* ошибка компиляции */ 49 return 1; 50 else 51 optind++; В строке 29 устанавливается значение myname, а строки 30-45 анализируют опции. Строки 47-51 компилируют регулярное выражение, помещая результаты в pattern. compile_pattern () увеличивает значение errors, если была проблема. (Соединение функций посредством глобальной переменной, как здесь, обычно считается плохой манерой. Для небольших программ, подобным этой, это сойдет, но для более крупных программ такое сопряжение может стать проблемой.) Если не было ошибок, строка 51 увеличивает значение optind так, что оставшиеся аргументы представляют файлы для обработки. 53 if (optind == argc) /* файлов нет, по умолчанию stdin */ 54 process("standard input", stdin);
454 Глава 12. Общие библиотечные интерфейсы - часть 2 55 else { 56 /* цикл с файлами */ 57 for (i = optind; i < argc; i++) { 58 if (strcmp(argv[i], "-") == 0) 59 process("standard input", stdin); 60 else if ((fp = fopen(argv[i], "r")) != NULL) { 61 process(argv[i], fp) ; 62 fclose(fp); 63 } else { 64 fprintf(stderr, "%s: %s: could not open: %s\n", 65 argv[0], argv[i], strerror(errno)); 66 errors++; 67 } 68 ' } 69 } 70 71 regfree(& pattern); 72 return errors != 0; 73 } Строки 53-69 обрабатывают файлы, отыскивая соответствующие шаблону строки. Строки 53-54 обрабатывают случай, когда файлы не указаны: программа читает со стандартного ввода. В противном случае, строки 57-68 обрабатывают в цикле файлы. Строка 58 обрабатывает особый случай '-', обозначающий стандартный ввод, строки 60-62 обрабатывают обычные файлы, а строки 63-67 обрабатывают ошибки. 7 5 /* compile_pattern компиляция шаблона */ . 76 77 void compile_pattern(const char *pat) 78 { 79 int flags = REG_NOSUB; /* информация о месте совпадения не требуется */ 80 int ret'; 81 #define MSGBUFSIZE 512 /* произвольно */ 82 char error[MSGBUFSIZE]; 83 84 if (ignore_case) 85 flags |= REG_ICASE; 86 if (extended) 87 flags |= REG_EXTENDED; 88 89 ret = regcomp(& pattern, pat, flags); 90 if (ret != 0) { 91 (void) regerror(ret, & pattern, error, sizeof error); 92 fprintf(stderr, "%s: pattern v%s': %s\n", myname, pat, error);
12.8. Регулярные выражения 455 93 errors++; 94 } 95 } . • Строки 75-95 определяют функцию compile_pattern.(). Она сначала устанавливает REG_NOSUB в flags, поскольку нам нужно знать лишь «подходит ли строка?», а не «где в строке располагается подходящий текст?» Строки 84-85 добавляют дополнительные флаги в соответствии с опциями командной строки. Строка 89 компилирует шаблон, а строки 90-94 сообщают о возникших ошибках. 97 •/* process читает строки текста и сопоставляет их с шаблоном */ 98 99 void process(const char *name, FILE *fp) 100 { 101 char *buf = NULL; 102 size_t size = 0; 103 char error[MSGBUFSIZE]; 104 int ret; 105 106 while (getline(& buf, &size, fp) != -1) { 107 ret = regexec(& pattern, buf, 0, NULL, 0); 108 • if (ret != 0) { 109 if (ret != REG_NOMATCH) { 110 (void) regerror(ret, & pattern, error, sizeof error); 111 fprintf(stderr, "%s: file %s: %s\n", myname, name, error); 112 free(buf); 113 errors++; 114 return; 115 } 116 } else 117 printf("%s: %s", name, buf); /* вывести подходящие строки */ 119 } 119 free(buf); 120 } Строки 97-120 определяют функцию process (), которая читает файл и выполняет сопоставление с регулярным выражением. Внешний цикл (строки 106-119) читает строки ввода. Для избежания проблем с длиной строки мы используем get line () (см. раздел 3.2.1.9 «Только GLIBC: чтение целых строк: get line () и getdelim() »). Строка 107 вызывает regexec(). Ненулевое возвращаемое значение означает либо неудачное сопоставление, либо какую-нибудь другую ошибку. Строки 109-115 соответственно проверяют REG_NOMATCH и выводят ошибку лишь тогда, когда возникла какая-нибудь другая проблема - неудачное сопоставление не является ошибкой. Если возвращаемое значение равно 0, строка совпала с шаблоном и соответственно строка 117 выводит имя файла и совпавшую строку.
456 Глава 12. Общие библиотечные интерфейсы - часть 2 122 /* usage вывод сообщения об использовании и выход */ 123 124 void usage(void) 125 { 126 fprintf(stderr, "usage: %s [-i] [-E] pattern [ files ... ]\n", myname) ; 127 exit(l) 128 } Функция usage () выводит сообщение об использовании и завершает програму. Она вызывается, когда предоставлены недействительные аргументы или не предоставлен шаблон (строки 38-40 и 44^5). Вот и все! Скромная, но тем не менее полезная версия grep в 130 строк кода. 12.9. Рекомендуемая литература 1. Programming Pearls, 2nd edition, by Jon Louis Bentley. Addison-Wesley, Reading, Massachusetts, USA, 2000. ISBN: 0-201-65788-0. См. также веб-сайт этой книги10. Проектирование программы с операторами проверки является одной из главных тем в этой книге. 2. Building Secure Software: How to Avoid Security Problems the Right Way, by John Viega and Gary McGraw. Addison-Wesley, Reading, Massachusetts, USA, 2001. ISBN: 0-201 -72152-X. Состояния гонки являются одной из многих проблем, о которых нужно побеспокоиться при написании безопасного программного обеспечения. Другой проблемой являются случайные числа. Данная книга рассматривает наряду с другими обе эти проблемы. (Мы упоминали о ней в предыдущей главе.) 3. The Art of Computer Programming: Volume 2: Seminumerical Algorithms, 3 edition, by Donald E. Knuth. Addison-Wesley, Reading, Massachusetts, USA, 1998. ISBN: 0-201- 89684-2п.См. также веб-сайт этой книги . Это классическое справочное руководство по генерации случайных чисел. 4. Random Number Generation and Monte Carlo Methods, 2nd edition, by James E. Gentle. Springer-Verlag, Berlin, Germany, 2003. ISBN: 0-387-00178-6. Данная книга широко освещает методы генерации и тестирования псевдослучайных чисел. Хотя для нее также требуется математическая и статистическая подготовка, уровень не такой высокий, как в книге Кнута. (Благодарим Nelson H.F. Beebe за указание этой ссылки.) 5. sed & awk, 2nd edition, by Dale Dougherty and Arnold Robbins. O'Reilly and Associates, Sebastopol, California, USA, 1997. ISBN: 1-56592-225-5. http : //www. cs .bell-labs . com/cm/cs/pearls/ -Примеч. автора. Русский перевод: Дональд Е. Кнут. Искусство программирования. Том 2. Получисленные алгоритмы C-е издание). Москва - Санкт-Петербург - Киев, Вильяме, 2000. - Примеч. науч. ред. http : / /www-cs-f acuity. Stanford, edu/-knuth/taocp .html -Примеч. автора.
12.10. Резюме 457 Эта книга осторожно вводит в регулярные выражения и обработку текста, начиная с grep и продвигаясь к более мощным инструментам sed и awk. 6. Mastering Regular Expressions, 2nd edition, by Jeffrey E.F. Friedl. O'Reilly and Associates, Sebastopol, California, USA, 200213. ISBN: 0-59600-289-0. Регулярные выражения являются важной частью Unix. Чтобы научиться заменять, вырезать и распределять текст с использованием регулярных выражений, мы рекомендуем эту книгу. 7. Руководство для GNU grep также объясняет регулярные выражения. На системе GNU/ Linux для просмотра локальной копии вы можете использовать 'info grep'. Или использовать браузер для прочтения онлайн-документации проекта GNU для grep14. 12.10. Резюме • Операторы проверки предоставляют способ сделать утверждения о предполагаемом состоянии программы. Они являются полезным инструментом для проектирования и отладки и обычно должны оставаться в коде изделия. Однако, будьте внимательны, чтобы не перепутать операторы проверки с проверками возможных ошибок времени исполнения. Функции тетХХХО являются аналогичными более известным функциям strXXX(). Самой большой их ценностью является то, что они могут работать с двоичными данными; нулевые байты не отличаются от других байтов. Больше известна memcpy () против memmove (), обрабатывающей перекрывающиеся копии. Временные файлы полезны во многих приложениях. Функции API tmpfile() Hmkstemp() являются предпочтительными способами создания временных файлов, в то же время позволяя избежать состояния гонки и связанных с ней проблем безопасности. Многие программы для указания местоположения своих временных файлов используют переменную окружения TMPDIR, а если она не определена, приемлемое значение по умолчанию (обычно /tmp). Это хорошее соглашение, которое следует принять на вооружение в своих программах. Функция abort () посылает вызывающему процессу сигнал SIGABRT. Результатом является завершение процесса и создание дампа ядра, предположительно для отладки. • set jmp () и longjmp () обеспечивают нелокальный переход. Это мощная возможность, которая должна использоваться с осторожностью. sigsetjmpO и siglongjmpf) сохраняют и восстанавливают маску сигналов процесса, когда программа осуществляет нелокальный переход. Проблемы с нелокальными переходами иногда перевешивают их преимущества; соответственно используйте эти процедуры лишь когда нет лучшего способа структурировать ваше приложение. Случайные числа полезны для множества приложений. Большинство программ используют псевдослучайные числа - последовательности номеров, которые кажутся случайными, но которые могут быть воспроизведены с помощью одного и того же начального значения. rand() и srand() являются первоначальными функциями 3 Русский перевод - Дж. Фридл. Регулярные выражения B-е издание). С. Петербург, Питер, 2003. - Примеч. науч. ред. 4 http: //www, gnu. org/sof tware/grep/doc/grep. html -Примеч. автора
458 Глава 12. Общие библиотечные интерфейсы - часть 2 API, стандартизованными языком С. На многих системах rand () использует низкокачественный алгоритм, random () и srandomO используют лучший алгоритм, включены в стандарт POSIX и являются предпочтительными по сравнению с rand () nsrandO. Используйте специальные файлы /dev/random и /dev/urandom, если (а) они доступны и (б) если вам нужны случайные числа высокого качества. Три функции API предоставляют все более мощные возможности для развертывания метасимволов (подстановки символов). • f nmatch () является простейшей, возвращающей true/false, если данная строка соответствует или не соответствует шаблону символов подстановки оболочки. • glob() просматривает файловую систему, возвращая список путей, которые соответствуют данному шаблону. Когда требуются стандартные возможности glob (), следует использовать эту функцию. Хотя GLIBC версия glob() имеет некоторые расширения, переносимые программы, которым нужны дополнительные возможности, должны вместо этого использовать wordexp (). (Программы, которые будут работать лишь на системах GNU/Linux, не должны стесняться использовать полную мощь GLIBC glob ().) • wordexp () не только делает то, что делает glob О , но также выполняет полное развертывание слов в стиле оболочки, включая развертывание тильды, развертывание переменных оболочки и подстановку команд. • Функции regcomp () и regexec () обеспечивают доступ к базовым и расширенным регулярным выражениям POSIX. Используя одну из этих функций, можно заставить свою программу вести себя идентично со стандартными утилитами, значительно упрощая использование программы пользователями, знакомыми с GNU/Linux и Unix. Упражнения 1. Используйте read () и memcmp () для написания простой версии программы стр, которая сравнивает два файла. Вашей версии не нужно поддерживать какие-нибудь опции. 2. Используйте макрос <stdio.h> getc () и прямое сравнение каждого прочитанного символа для написания другой версии стр, которая сравнивает два файла. Сравните производительность этой версии с производительностью написанной в предыдущем упражнении. 3. (Средней трудности) Рассмотрите функции <std'io.h> fgetsO и GLIBC getline (). Полезна ли memcpy () для их реализации? Набросайте с ее использованием возможную реализацию f gets (). 4. (Трудное) Найдите исходный код GLIBC версии memcmp (). Он должен быть на одном из CD-ROM с исходным кодом в вашем дистрибутиве GNU/Linux, или же вы можете найти его в сети. Исследуйте код и объясните его. 5. Проверьте свою память. Как tmpfileO организует удаление файла, когда закрыт указатель файла? 6. Используя mkstemp () и f dopen (), а также другие необходимые функции или системные вызовы, напишите свою версию tmpf ile (). Протестируйте ее тоже. 7. Опишите преимущества и недостатки использования unlink() для имени файла, созданного mkstemp (), непосредственно после возвращения mkstemp ().
Упражнения 459 8. Напишите свою версию mkstemp (.), используя mktemp () и open (). Как вы можете обеспечить те же гарантии уникальности, которые обеспечивает mkstemp () ? 9. Программы, использующие mkstemp (), должны обеспечивать удаление файла при завершении. (Предположим, что файл не удаляется сразу же после открытия по тем или иным причинам.) Это включает в себя случай, когда может поступить сигнал завершения. Поэтому удаление файла должно быть частью перехватчика сигнала. Как вы это сделаете? 10. (Трудное) Даже с урезанной очисткой при обработке сигнала все еще имеется состояние гонки. Есть небольшое окно между созданием временного файла функцией mkstemp (). и возвращением и записью его имени в переменной (для'использования функцией обработки сигнала). Если в это окно попадает не перехваченный сигнал, программа завершается и оставляет временный файл. Как вы закроете это окно? (Спасибо Jim Meyering.) 11. Попробуйте откомпилировать и запустить chl2-setjmp.c на как можно большем количестве различных систем с использованием как можно большего количества различных компиляторов, к каким у вас есть доступ. Попробуйте компилировать с различными уровнями оптимизации. Какие изменения поведения вы видели (если они были)? 12. Посмотрите файл /usr/src/libc/gen/sleep.c в дистрибутиве исходного кода V7 Unix. Он содержит реализацию функции sleepO, описанную в разделе 10.8.1 «Сигнальные часы: sleep (), alarm () и SIGALARM». Распечатайте ее и прокомментируйте в стиле наших примеров, чтобы объяснить ее работу. 13. Посмотрите справочную страницу lrand48C) на системе GNU/Linux или System V. Выглядит ли этот интерфейс более простым или трудным для использования, чем random () ? 14. Возьмите ch08-nftw.c из раздела 8.4.3 «Перемещение по иерархии: nftwO» и добавьте опцию --ехс1и<3е=паттерн. Файлы, соответствующие паттерну, не должны выводиться. 15. (Трудное) Почему GLIBC нужны указатели на альтернативные версии функций стандартных каталогов и stat () ? Не может ли она вызывать их непосредственно? 16. Измените chl2-glob.c для использования функции'wordexp (). Поэкспериментируйте с ней, проделав несколько дополнительных вещей, которые она предоставляет. Не забудьте взять аргументы командной строки в кавычки, чтобы wordexp () на самом деле выполнила свою работу! 17. Стандартная grep выводит имя файла, лишь когда в командной строке указано больше одного файла. Сделайте так, чтобы chl2 -grep. с действовала таким же образом. 18.Посмотрите справочную страницу grep(\). Добавьте к chl2-grep.c стандартные опции -е, -s и -v. 19. Напишите простую замещающую программу: subst [-g] шаблон подстановка [файлы ...] Она должна читать текстовые строки из указанных файлов или из стандартного ввода, если они не указаны. Каждая строка должна сравниваться на соответствие шаблону. Если обнаружено соответствие, оно должно замещаться подстановкой. Если указана опция -д, замещаться должно не только первое совпадение, но и все остальные совпадения в строке.
Глава 13 Интернационализация и локализация В этой главе: • 13.L Введение. . . .• 460 • 13.2. Локали и библиотека С \ 461 • 13.3. Динамический перевод сообщений программ 480 • 13.4. Не могли бы вы произнести это для меня по слогам? 493 • 13.5. Рекомендуемая литература 497 • 13.6. Резюме 497 • Упражнения. 498 ж*анние вычислительные системы обычно для своего вывода (приглашений, сообщений об ошибках) и ввода (ответы на запросы, такие, как «да» и «нет») использовали английский язык. Это было верно для систем Unix вплоть до середины 1980-х. В конце 80-х, начиная с первого стандарта ISO для С и продолжая стандартами POSIX 1990-х и современным стандартом POSIX, были разработаны возможности для работы программ на нескольких языках без необходимости поддержки нескольких версий одной и той же программы. Данная глава описывает, как современные программы должны справляться с многоязычными проблемами. 13.1. Введение Центральной концепцией является окружение, место, в котором работает программа. Локали содержат в себе следующие сведения: локальный набор символов; сведения о формате отображения даты и времени; форматирование и отображение денежных сумм; форматирование и отображение числовых значений (с или без разделителей тысяч, какой символ используется в качестве разделителя дробной части числа и т. д.). Интернационализация является процессом написания (или изменения) программы таким образом, что она может работать с различными локалями. Локализация является процессом приспособления интернационализированной программы для определенной локали. Часто вместо этих терминов используют сокращения И8п и И On соответственно. (Числовое значение указывает, сколько букв в середине слова, а эти сокращения имеют небольшое сходство с полными терминами1. Их также гораздо легче набирать.) Другим От английских слов internationalizatio)n и l(ocalizatio)n. -Примеч. перев.
13.2. Локали и библиотека С 461 часто встречающимся термином является 1поддержка родного языка, обозначаемая как NLS2; NLS обозначает программную поддержку для il8n и ИОп. Кроме того, некоторые люди используют термин глобализация (сокращенно glOri) для обозначения процесса подготовки.йсех возможных локализаций для интернационализированной программы. Другими словами, подготовки программы для глобального использования. Возможности NLS существуют на двух уровнях. Первым уровнем является библиотека С. Она предоставляет сведения о локали; процедуры для обработки большей части низкоуровневых подробностей работы по форматированию даты/времени, числовых и денежных значений; и процедуры для корректного для данной локали сопоставления регулярных выражений и классификации символов и сравнений. Именно возможности библиотеки появляются в стандартах С и POSIX. На уровне приложения GNU gettext предоставляет команды и библиотеку для локализации программы: т. е. для возможности вывода сообщений на одном или более естественных языках. GNU gettext основана на плане, первоначально разработанном Sun Microsystems для Solaris3; однако, она была реализована с нуля и теперь предоставляет расширения к первоначальному gettext Solaris. GNU gettext является стандартом де-факто для локализации программ, особенно в мире GNU. В дополнение к локалям и gettext стандарт С предоставляет возможности для работы с несколькими наборами символов и с их кодировками - способом представления больших наборов символов с помощью меньшего числа байтов. Мы кратко затронем эти проблемы в конце главы. 13.2. Локали и библиотека С Специфичное для локали поведение управляется посредством установки переменных окружения, описывающих, какую локаль (локали) использовать для той или иной информации. Число доступных локалей, предлагаемых каждой конкретной операционной системой, колеблется от менее чем десяти на некоторых коммерческих системах Unix до сотен локалей на системах GNU/Linux. (' locale -а' выводит полный список доступных локалей.) Гарантируется существование двух локалей, «С» и «PGSIX». Они действуют в качестве локали по умолчанию, предоставляя окружение 7-разряДНого ASCII, поведение которого такое же, как на традиционных системах Unix без поддержки локалей. В противном случае, локали обозначают язык, страну, а также могут включать сведения о наборе символов. Например, 'it_IT' используется для итальянского языка в Италии с использованием системного набора символов по умолчанию, a 4it_JT.UTF-8' использует кодировку UTF-8 для набора символов Unicode. 2 NLS - native language support. - Примеч. перев. 3 Существует более ранний дизайн, известный как catgets(). Хотя он стандартизован POSIX, его гораздо сложнее использовать, и мы его не рекомендуем. - Примеч. автора.
462 Глава 13. Интернационализация и локализация Дополнительные подробности об именах локалей можно найти в справочной странице GNU/Linux setlocaleC). Обычно дистрибутивы GNU/Linux устанавливают для системы локаль по умолчанию при ее установке, основываясь на языке, выбранном тем кто устанавливал ее, и пользователям больше не приходится об этом беспокоиться. 13.2.1. Категории локалей и переменные окружения Заголовочный файл <locale.h> определяет функции и структуры локали. Категории локали определяют разновидности информации, которые будут для программы зависимы от локали. Категории доступны в виде набора именованных констант. Они перечислены в табл. 13.1. Таблица 13.1. Константы категорий локалей ISO С, определенные в <locale .h> Категория Значение LC__ALL Эта категория включает всю возможную информацию локали. Она состоит из оставшейся части элементов этой таблицы. LC_COLLATE Категория для сравнения строк (обсуждаемого ниже) и областей регулярных выражений. LC_CTYPE Категория для классификации символов (заглавные, строчные и т. д.) Это влияет на сопоставление регулярных выражений и функции isXXXO B<ctype.h>. LC_MESSAGES Категория для специфичных для локали сообщений. Эта категория вступает в игру с GNU gettext, которая обсуждает далее в главе. LC_MONETARY Категория для форматирования денежной информации, такой, как локальные и международные символы для местной валюты (например, $ против USD для доллара США), форматирования отрицательных величин и т. д. LC_NUMERIC Категория для форматирования числовых значений. LC_TIME Категория для форматирования дат и времени. Эти категории определены различными стандартами. Некоторые системы могут поддерживать дополнительные категории, такие, как LC_TELEPHONE или LC_ADDRESS. Однако, они не стандартизованы; любой программе, которой нужно их использовать, но которая все равно должна быть переносимой, следует использовать #ifdef для окружения соответствующих разделов. По умолчанию, программы С и библиотека С ведут себя так, как если бы они находились в локали «С» или «POSIX» для обеспечения обратной совместимости со старыми системами. Однако, вызвав setlocaleO (как описано ниже), программа может включить действие локали. После того, как программа это сделала, пользователь может, установив переменные окружения, включать и выключать возможности локали, которые будет иметь программа.
13.2. Локали и библиотека С 463 Переменные окружения имеют те же самые имена, что и перечисленные в табл. 13.1 категории л окал ей. Таким образом, команда - export LC_NUMERIC=en_DK LC__TIME=C - определяет, что числа должны выводиться в соответствии с локалью cen_DK' (английский язык в Дании), но что значения даты и времени должны выводиться в соответствии с обычной локалью 4С\ (Этот пример просто иллюстрирует, что вы можете указывать для различных категорий различные локали; это не является чем-то обязательным, что вы должны делать.) Переменная окружения LC_ALL перекрывает все другие переменные ЬС_ххх. Если LC_ALL не установлена, библиотека ищет определенные переменные (LC_CTYPE, LC__MONETARY и т. д.). Наконец, если ни одна из них не установлена, библиотека ищет переменную LANG. Вот небольшая демонстрация с использованием gawk: $ unset LC_ALL LANG /* Удалить переменные по умолчанию */ $ export LC_NUMEIRIC==enJDK LCJPIME=C /* Европейские числа, дата и время по умолчанию */ $ gavdc 'BEGIN { print 1.234 ; print strftimeO }' /* Вьвести число, текущие дату и вршя */ 1,234 Wed Jul 09 09:32:18 PDT 2003 $ export LC_NUMERIC=it_IT LC_TIME=sit__IT / * Итальянские числа, дата и время * / $ gawk 'BEGIN { print 1.234 ; print strftimeO }' /* Вьвести число, текущее дату и время */ 1,234 mer lug 09 09:32:40 PDT 2003 $ export LC_ALL=C /* Установить перекрывающую переменную */ $ gawk 'BEGIN { print 1.234 ; print strftimeO }' /* Вьвести число, текуиие дату и вреля V 1.234 Wed Jul 09 09:33:00 PDT 2003 Для awk стандарт POSIX констатирует, что числовые константы в исходном коде всегда используют в качестве десятичного разделителя '.', тогда как числовой вывод следует правилам локали). Почти все GNU версии стандартных утилит Unix могут использовать локали. Таким образом, особенно на системах GNU/Linux, установка этих переменных позволяет вам контролировать поведение системы4. 13.2.2. Установка локали: set locale О Как уже упоминалось, если вы ничего не делаете, программы на С и библиотека С ведет себя так, как если бы использовалась локаль «С». Функция setlocale () устанавливает соответствующую локаль: #include <locale.h> /* ISO С */ char *setlocale(int category, const char *locale); 4 Программисты, долгое время работавшие на С и Unix, могут предпочесть использовать локаль *С\ даже если их родной язык английский; английские локали дают другой результат по сравнению с тем, что ожидают эти седые, понюхавшие пороху ветераны Unix. - Примеч. автора.
464 Глава 13. Интернационализация и локализация Аргумент category является одной из категорий, описанных в разделе 13.2.1 «Категории локалей и переменные окружения». Аргумент locale является строкой, именующей используемую для этой категории локаль. Когда locale является пустой строкой (""), setlocale () проверяет соответствующие переменные окружения. Если locale равно NULL, сведения о локали не изменяются. Вместо этого функция возвращает строку, представляющую текущую локаль для данной категории. Поскольку каждая категория может быть установлена индивидуально, автор приложения решает, насколько будет программа использовать локаль. Например, если main() делает лишь это - . setlocale (LC_TIME, ""); /* Использование локали только для времени и все */ - тогда, независимо от установленных в окружении других переменных LC_xxx, локали подчиняются лишь функции времени и даты. Все остальные действуют так, как если бы программа по-прежнему работала в локали «С». Сходным образом вызов: setlocale(LC_TIME, "it_IT"); /* -Время всегда итальянское */ заменяет переменную окружения LC_TIME (также, как LC_ALL), заставляя программу использовать для вычислений времени/даты данные для Италии. (Хотя Италия может быть прекрасным местом, программам лучше использовать "", чтобы они могли корректно работать везде; этот пример предназначен лишь для объяснения того, как работает setlocale ().) Можно индивидуально вызывать setlocale () для каждой категории, но простейшим способом является установка всего одним махом: . /* Находясь в Риме, вместо «всего» делайте все как римляне. :-) */ .setlocale(LC_ALL, ""); Возвращаемое setlocale () значение является текущей установкой локали. Это либо строковое значение, переданное в предыдущем вызове, либо непрозрачное значение, представляющее используемую вначале локаль. Это самое значение может быть затем передано обратно setlocale (). Для последующего использования возвращаемое значение должно быть скопировано в локальное хранилище, поскольку это указатель на внутренние данные: char *initial__locale; initial__locale = strdup(setlocale(LC_ALL, "") ) ; /* сохранить копию */ (void) setlocale(LC__ALL, initial_locale); /* восстановить ее */ Здесь мы сохранили копию, использовав функцию POSIX strdup () (см. раздел .3.2.2 «Копирование строк: strdup () »). 13.2.3. Сравнение строк: strcoll () и strxf rm() Знакомая функция s trcmp () сравнивает две строки, возвращая отрицательное, нулевое или положительное значения, если первая строка меньше, равна или больше второй. Это сравнение основано на числовых значениях символов в машинном наборе символов. Из-за этого результаты strcmp () никогда не изменяются.
13.2. Локали и библиотека С 465 Однако, при наличии локалей простого числового сравнения недостаточно. Каждая локаль определяет для содержащихся в ней символов последовательность сортировки, другими словами, относительный порядок символов внутри локали. Например, в простом 7-битном ASCII у двух символов 4А' и 4а' десятичные значения равны 65 и 97 соответственно. Соответственно, во фрагменте int i = strcmp("A" , "а"); i имеет отрицательное значение. Однако, в локали "en_US .UTF-8" 'А' идет после 'а', а не перед ним. Таким образом, использование strcmp () для приложений, использующих локаль, является плохой мыслью; мы могли бы сказать, что она возвращает игнорирующий локаль ответ. Функция strcoll () (string collate - сортировка строк) существует для сравнения строк с использованием локали: #include <string.h> /* ISO С */ int strcoll(const char *sl, const char *s2); Она возвращает такие же отрицательные/нулевые/положительные значения, что и strcmp (). Следующая программа, chl3 -compare. с, интерактивно демонстрирует разницу: I /* chl3-compare.с демонстрация strcmp() против strcoll() */ 2 3 #include <stdio.h> 4 #include <locale.h> 5 #include <string.h> 6 7 int main(void) 8 { 9 #define STRBUFSIZE 1024 10 char locale[STRBUFSIZE], curloc[STRBUFSIZE]; II char left[STRBUFSIZE], right[STRBUFSIZE]; 12 char buf[BUFSIZ]; 13 int.count; 14 15 setlocale(LC_ALL, ""); /* установить локаль */ 16 strcpy(curloc, setlocale(LC_ALL, NULL)); /* сохранить ее */ 17 18 printf("--> "); fflush(stdout); 19 while (fgets(buf, sizeof buf, stdin) != NULL) { 20 locale[0] = ' \0'; 21 count = sscanf(buf/ "%s %s %s", left, right, locale); 22 if (count < 2) 23 break; 24 25 if (*locale) { 26 setlocale(LC_ALL, locale); \
466 Глава 13. Интернационализация и локализация 27 strcpy(curloc, locale); 28 } 29 30 printf("%s: strcmp(\"%s\", \"%s\") is %d\n"/ curloc, left, 31 right, strcmpdeft, right)); 32 printf("%s: strcoll(\"%s\", \"%s\") is %d\n"/ curloc, left, 33 right, strcoll(left, right)); 34 3 5 printf("\n--> "); fflush(stdout); 36 } 37 3 8 exit@); 39 } Программа читает входные строки, состоящие из двух сравниваемых слов и необязательной локали, использующейся для сравнения. Если локаль дана, она становится локалью для последующих элементов. Программа начинает с любой локалью, которая установлена в окружении. Массив curloc сохраняет текущую локаль для вывода результатов; left и right являются левым и правым сравниваемыми словами (строки 10-11). Основную часть программы составляет цикл (строки 19-36), который читает строки и выполняет работу. Строки 20-23 разделяют входную строку, locale инициализируется пустой строкой, если третья строка не предусмотрена. Строки 25-28 устанавливают новую локаль, если она приведена. Строки 30-33 выводят результаты сравнения, а строка 35 приглашает для дальнейшего ввода. Вот демонстрация: $ chl3-compare /* Запуск программы •*/ --> ABC abc /* Ввести два слова */ . С: strcmp("ABC", "abc") is -1 /* Программа началась в локали "С" */ С: strcoll("ABC", "abc") is -Г /* В локали "С" идентичные рез-ты */ --> ABC abc en_US /* Слова те же, локаль "en_US" */ en_US: strcmp("ABC", "abc") is -1 /* strcmpO без изменений */ en_US: strcoll("ABC", "abc") is 2 /* рез-ты strcoll() изменились! */ --> ABC abc en_US.UTF-8 /* Слова те же, локаль "en_US.UTF-8" */ en_US.UTF-8: strcmp("ABC", "abc") is -1 ' en_US.UTF-8: strcoll("ABC", "abc") is б /* Другое значение, все еще положительное */ --> junk JUNK /* Новые слова */ en_US.UTF-8: strcmp("junk", "JUNK") is 1 /* предыдущая локаль */ en_US.UTF-8: strcoll("junk", "JUNK") is -6 Эта программа ясно показывает различие между strcmp () и strcoll (). Поскольку strcmp () работает в соответствии с числовыми значениями символов, она всегда возвращает тот же самый результат, strcoll () понимает проблемы сортировки, и ее результат
13.2. Локали и библиотека С 467 меняется в соответствии с локалью. Мы видим, что в обеих локалях en_US заглавные буквы идут после строчных. 13АМЕЧАНИЕ. Специфическая для локали сортировка строк является проблемой I также и для сопоставления регулярных выражений. Регулярные выражения допус- Хкают диапазоны символов внутри выражений со скобками, такие, как ' [a-z]' или I' [ " -/] '. Точное значение такой конструкции (символы, численно располагающиеся Iмежду начальной и конечной точками включительно) определено лишь для локалей I «С» и «POSIX». IДля локалей, не являющихся ASCII, такие диапазоны как ' [a-z] 'могут соответствовать также и заглавным буквам, а не только строчным! Диапазон '["-/]' I действителен в ASCII, но не в "en_US. UTF-8". {Долговременным наиболее переносимым решением является использование классов IсимволовPOSIX, таких, как '[ [ .-lower: ] ] ' и ' [ [ :punct: ] ] '. Если вам кажется, \что нужно использовать выражения с диапазонами на системах, использующих Iлокали, и на более старых системах, не использующих их, без изменения своей про- I граммы, решение заключается в применении грубой силы и индивидуальном перечислении каждого символа внутри скобок. Это неприятно, но это работает. Основанная на локалях сортировка потенциально дорогостоящая. Если вы ожидаете большого числа сравнений, где по крайней мере одна из строк не будет изменяться или где значения строк будут сравниваться друг с другом по несколько раз (как при сортировке списка), следует рассмотреть использование функции strxfrmO для преобразования своих строк для использования с strcmpO. Функция strxfrmO объявлена следующим образом: #include <string.h> /* ISO С */ size_t strxfrm(char *dest, const char *src, size_t n); Идея в том, что strxf rm() преобразует первые п символов src, помещая их в dest. Возвращаемое значение является числом символов, необходимых для сохранения преобразованных символов. Если она превышает п, содержимое dest «неопределенно». Стандарт POSIX явным образом разрешает устанавливать в п ноль, а в dest NULL. В этом случае strxf rm() возвращает размер массива, необходимого для сохранения преобразованной версии src (не включая завершающий символ '\0'). Предполагается, что это значение впоследствии будет использовано с malloc () для создания массива dest или для проверки размера предопределенных границ массива. (При этом, очевидно, src должен иметь завершающий нулевой байт.) Этот фрагмент иллюстрирует использование strxf rm (): #define STRBUFSIZE ... char si[STRBUFSIZE], s2[STRBUFSIZE]; /* Оригинальные строки */ char six[STRBUFSIZE], s2x[STRBUFSIZE}; /* Преобразованные копии */ size_t lenl, len2 ; int cmp; /* ... заполнить si и s2 ... */
468 Глава 13. Интернационализация и локализация lenl = strlen(sl); 1еп2 = strlen(s2); if (strxfrm(slx, si, lenl) >= STRBUFSIZE || strxfrm(s2x, s2, len2) >= STRBUFSIZE) /*. слишком большой, восстановить */ cmp = strcmp(slx/ s2x); if (cmp ==0) /* равны */ else if (cmp < 0) /* si < s2 */ else /* si > s2 */ Для одноразовых сравнений, возможно, быстрее непосредственно использовать strcoll().Ho если строки будут сравниваться несколько раз, более быстрым будет использование сначала strxf rm (), а затем strcmp () с преобразованными значениями. Функций для локали, соответствующих strncmp () или strcasecmp (), нет. 13.2.4. Числовое и денежное низкоуровневое форматирование: localeconvO Корректное форматирование числовых и денежных значений требует значительной низкоуровневой информации. Указанная информация доступна в struct iconv, которую получают с помощью функции localeconv (): #include <locale.h> /* ISO С */ struct Iconv *localeconv(void); Подобно функции с time (), эта функция возвращает указатель на внутренние статические данные. Следует сделать копию возвращенных данных, поскольку последующие вызовы могут возвратить другие значения, если локаль изменилась. Вот struct Iconv (слегка сжатая), непосредственно из GLIBC <1оса1е. п>: struct Iconv { /* Числовая (не денежная) информация. */ char *decimal_point; /* Разделитель десятичной дроби. */ char *thousands_sep; /* Разделитель тысяч.'*/ /* Каждый элемент является числом цифр в каждой группе; элементы с большими индексами оставлены дальше. Элемент со значением CHAR__MAX означает, что дальнейшая группировка не производится. Элемент со значением 0 означает, что предыдущий элемент используется для всех оставшихся групп. */ char *grouping; /* Денежная информация. *'/ /* Первые три символа являются символами валют из ISO 4217. Четвертый символ является разделителем. Пятый символ ' \0'. */
13.2. Локали и библиотека С char *int__curr_symbol; char *currency_symbol; /* Символ местной валюты. */ char *mon_decimal_point; /* Символ десятичной точки. */ char *mon__thousands_sep; /* Разделитель тысяч. */ char *mon_grouping; /* Аналогично элементу * группировки' (выше). char *positive_sign; /* Знак для положительных значений. */* char *negative_sign; /* Знак для отрицательных значений. */ char int_frac_digits; /* Международные цифры дробей. */ char frac_digits; /* Местные цифры дробей. */ /* 1, если символ валюты перед положит, значением/ 0, если после. char p__cs__precedes ; /* 1, если символ валюты отделяется от положит, значения пробелом. char p_sep_by_space; /* 1/ если символ валюты перед отриц. значением, 0, если после. */ char n_cs_precedes; /* 1, если символ валюты отделяется от отриц. значения пробелом. * char n_sep_by_space; /* Размещение.положительного и отрицательного знака: 0 Количество и символ валюты окружены скобками. 1 Строка знака перед количеством и символом валюты. 2 Строка знака за количеством и символом валюты. 3 Строка знака непосредственно перед символом валюты. 4 Строка знака непосредственно после символа валюты. */ char p_sign_posn; char n_sign_posn; /* I, если int_curr_symbol до положит, значения/ 0, если после. */ char int_p_cs_precedes; /* I, если int_curr_symbol отделен от положит, знач. пробелом.'*/ char int_p_sep_by_space; /* 1, если int_curr_symbol перед отриц. значением, 0, если после. char int_n_cs_j?recedes; /* I, если int__curr_symbol отделен от отриц. знач. пробелом. */ char int_n_sep_by_space; /* Размещение положительного и отрицательного знака: 0 Количество и int_curr_symbol окружены скобками. 1 Строка знака до количества и int_curr_symbol. 2 Строка знака после количества и int_curr__symbol. 3 Строка знака непосредственно до int_curr_symbol. 4 Строка знака непосредственно после int_curr_symbol. */ char int_p_sign_jposn; char int_n_j3ign_posn;
470 Глава 13. Интернационализация и локализация Комментарии показывают довольно ясно, что происходит. Давайте посмотрим на несколько первых полей struct lconv: decimal_point Используемый символ разделителя десятичной дроби, б Соединенных Штатах и других англоязычных странах это точка, но во многих странах используется запятая. thousands_sep Символ, используемый для разделения каждых 3 цифр значения. grouping Массив однобайтных целых значений. Каждый элемент указывает, сколько цифр в группе. Как сказано в комментарии, СНАБМУЕАХ означает, что дальше группировка не используется, а 0 означает повторное использование последнего элемента. (Далее в главе мы покажем пример кода.) int_curr_symbol Это международный символ для местной валюты. Например, 'USD' для доллара США. currency_symbol Локальный символ для местной валюты. Например, $ для доллара США. mon_decimal^point, mon_J:housands__sep, mon__grouping Соответствуют предыдущим полям, предоставляя те же сведения, но для денежных сумм. Большая часть оставшихся значений не имеет значения для повседневного программирования. Следующая программа, chl3-lconv.c, выводит некоторые из этих значений, чтобы дать вам представление, для какого рода сведений они используются: /.* chl3-lconv.c показывает некоторые компоненты struct lconv */ #include <stdio.h> #include <limits.h> #include <locale.h> int main(void) { struct lconv 1; int i ; setlocale(LC_ALL, ""); 1 = *localeconv(); printf ("decimal_point = [%s]\n"/ 1 .decimal__point) ; printf("thousands_sep = [%s]\n", 1.thousands_sep); for (i = 0; 1.grouping[i] != 0 && 1.grouping[i] != CHAR_MAX; i++) printf ("grouping [%d] = [%d]\n"/ i., 1.grouping[i]); printf("int_curr_symbol = [%s]\n", 1.int_curr_symbol); printf("currency_symbol = [%s]\n", 1.currency_symbol); printf ("mon_decimal_point = [%s] \n", 1 .mon_decimal__point) ; printf ("mon_thousands__sep = [%s] \n", 1 .mon_thousands_sep) ;
13.2. Локали и библиотека С 471 printf ("positive_sign = [%s]\n", 1 .positive__sign) ; printf ("negative__sign = [%s]\n"/ 1.negative_sign); } ^ Неудивительно, при запуске в различных локалях мы получаем различные результаты: $ LC_ALL=en_TJS chl3-lconv /* Результаты для Соединенных Штатов */ decimal_point = [.] thousands_sep = [, ] grouping[0] = [3] grouping[1] = [3] int_curr__symbol = [USD ] currency_symbol = [$] mon_decimal__point = ¦[.] mon_thousands_sep = [, ] positive_sign = [] negative_sign = [-] $ LC_ALLssit_.IT chl3-lconv /* Результаты для Италии */ decimal_point = [.] thousands_sep = [] int_curr_symbol = [] currency_symbol = [] mon_decimal_point = [] mon__thousands_sep = [] positive_sign = [] negative_sign = [] Обратите внимание, что значение int__curr_symbol в локали "en__US" включает завершающий символ пробела, который служит для отделения символа от последующего денежного значения. 13.2.5. Высокоуровневое числовое и денежное форматирование: strf топ () и printf () После рассмотрения всех полей struct lconv вы можете поинтересоваться: «Нужно ли мне на самом деле выяснять, как использовать все эти сведения, просто для форматирования денежного значения?» К счастью, ответом является «нет»5. Функция strf топ () делает за вас всю работу: #include <monetary.h> /* POSIX */ ssize_t strfmon(char *s, size_t max, const char *format, ...); Эта функция во многом подобна strf time () (см. раздел 6.1.3.2 «Сложное форматирование времени: strf time () »), используя format для копирования символов букв и форма- Мы так же счастливы, как и вы, поскольку нам не нужно представлять код, использующий з\у полнофункциональную структуру. - Примеч. автора.
472 Глава 13. Интернационализация и локализация тированных числовых значений в s, помещая в нее не более max символов. Следующая простая программа, chl3 -strfiron. с, демонстрирует работу str'fmon (): /* chl3-strfmon.c демонстрация strfmonO */ #include <stdio.h> #include <locale.h> #include <monetary.h> int main(void) { char buf[BUFSIZ]; double val = 1234.567; se t locale (LC__ALL, ""); strfmon(buf, sizeof buf, "You owe me %n (%i)\n", val, val); fputs(buf, stdout); return 0; } При запуске в двух различных локалях она выдает такой результат: $ LC_ALL=en_US chl3-strfmon /* В Соединенных Штатах */ You owe me $1,234.57 (USD 1,234.57) $ LC_ALL=it_IT chl3-strfmon /* В Италии */ You owe me EUR 1.235 (EUR 1.235) Как вы можете видеть, strfmonO подобна strf time О, копируя обычные символы в буфер назначения без изменений и форматируя аргументы в соответствии со своими собственными спецификациями форматирования. Их всего три: %п Вывести национальное (т. е. местное) представление значения валюты. %i Вывести международное представление значения валюты. %% Вывести символ '%'. Форматируемые значения должны иметь тип double. Разницу между %п и %i мы видим в локали "en_US"; %n использует символ $, тогда как %i использует USD, которая означает «доллары США». Гибкость - и соответственно определенная сложность - сопровождают многие функции API, разработанные для POSIX, и strfmonO не является исключением. Как и с printf (), несколько необязательных элементов, которые могут быть между % и i или п, обеспечивают повышенный контроль. Полные формы следующие: %[флаги] [ширина поля] [#точность_слева] [.точность_справа] i %[флаги] [ширина поля] [#точность__слева] [ . точность_справа]п %% /*.Не допускаются поля флагов, ширины и т.д. '*/ Флаги перечислены в табл. 13.2. .
13.2. Локали и библиотека С 473 Таблица 13.2. Флаги для strf mon () Флаг Значение =с Использовать символ с в качестве символа числового заполнения слева. Символом по умолчанию является пробел. Обычной альтернативой является 0. л Запретить использование символа группировки (например, запятой в Соединенных Штатах). ( Отрицательные значения заключать в скобки. Несовместим с флагом +. + Обрабатывать положительные/отрицательные значения обычным образом. Использовать положительные и отрицательные знаки локали. Несовместим с флагом (. ! Не включать символ валюты. Этот флаг полезен, если вы хотите использовать strf mon С) для более гибкого форматирования обычных чисел, чем это предусматривает sprintf (). Выровнять результат слева. По умолчанию используется выравнивание справа. Этот флаг не действует без указания ширины поля. Ширина поля является строкой десятичных цифр, представляющих минимальную ширину. По умолчанию использует столько символов, сколько необходимо, основываясь на оставшейся части спецификации. Значения, меньшие ширины поля, дополняются пробелами слева (или справа, если указан флаг '-'). Точность слева состоит из символа # и строки десятичных цифр. Она указывает минимальное число цифр, которые должны быть слева от десятичного символа-разделителя дробной части6; если преобразованное значение меньше этого, результат выравнивается символом числового заполнения. По умолчанию используется пробел, однако для его изменения можно использовать флаг =. Символы группировки не включаются в общий счет. Наконец, точность справа состоит из символа 4."' и строки десятичных цифр. Она указывает, с каким числом значащих цифр округлить значение до форматирования. По умолчанию используются поля f rac__digits и int_f rac__digits в struct lconv. Если это значение равно 0, десятичная точка не выводится. strf mon () возвращает число символов, помещенных в буфер, не включая завершающий нулевой байт. Если недостаточно места, функция возвращает-1 и устанавливает errno в E2BIG. Помимо strfmonO, POSIX (но не ISO С) предусматривает специальный флаг - символ одинарной кавычки, ' - для форматов printf () %i, %d, %u, %f, %F, %g и %G. В локалях, имеющих разделитель тысяч, этот флаг добавляет и его. Следующая простая программа, ch!3-quotef lag. с, демонстрирует вывод: В стандарте используется технический термин radix point (позиционный разделитель), поскольку числа с другим основанием счисления также могут иметь дробные части. Однако, для денежных значений можно довольно безопасно использовать термин 'decimal point' (десятичный разделитель). - Примеч. автора.
474 Глава 13. Интернационализация и локализация /* chl3-quotef lag. с демонстрация флага кавычки printf *'/ #include <stdio.h> ( #include <locale.h> int main(void) { < setlocale(LC_ALL, ""); /* Это нужно, иначе не будет работать */ printf(M%'d\n", 1234567); return 0; } ' Вот что происходит для двух различных локалей: в одной есть разделитель тысяч, в другой нет: $ LC_ALL=C chl3~quoteflag /* Обычное окружение, без разделителя */ 1234567 $ LC_ALL=en_US chl3-quoteflag /* Локаль с разделителем (англ.) */ 1,234,567 На время написания лишь GNU/Linux и Solaris поддерживают флаг '. Дважды проверьте справочную страницу printfib) на своей системе. 13.2.6. Пример: форматирование числовых значений в gawk gawk реализует свои собственные версии функций printf () и sprintf (). Для полного использования локали gawk должен поддерживать флаг', как в С. Следующий фрагмент из файла builtin. с в gawk 3.1.4 показывает, как gawk использует struct lconv для числового форматирования: 1 case 'd*: 2 case 'i' : 3 4 tmpval = force_number(arg); 5 6 7 uval = (uintmax_t) tmpval; 8 9 ii = jj = 0; 10 do { 11 *—cp = (char) ('0' + uval % 10); 12 #ifdef HAVE_LOCALE_H 13 if (quote_flag && loc.grouping[ii] && ++jj == loc.grouping[ii]) { 14 *—cp = loc.thousands_sep[0]; /* XXX - предполокение, что это один символ */ 15 if (loc.grouping[ii+1] == 0) 16 jj = 0; /* продолжить использовать текущий val в loc. grouping [ii] */ 17 else if (loc.grouping[ii+1] == CHAR_MAX) 18 quote_flag = FALSE; 19 else {
13.2. Локали и библиотека С 475 20 ii++; 21 jj = 0; 22 } 23 } 24 #endif , 25 uval /= 10; 26 } while (uval > 0); (Номера строк даны относительно начала фрагмента.) Некоторые части кода, не имеющие отношения к обсуждению, были опущены, чтобы облегчить фокусировку на важных частях. Переменная loc, используемая в строках 13-17, представляет struct lconv, Она инициализируется в main (). Здесь для нас интерес представляет loc. ?housands_sep, который является символом разделителя тысяч, и loc . grouping, который является массивом, описывающим число цифр между разделителями. Нулевой элемент означает «использовать для всех последующих цифр значение предыдущего элемента», а значение CHAR__MAX означает «прекратить вставку разделителей тысяч». С таким введением, давайте посмотрим на код. Строка 7 устанавливает uval, которая является беззнаковой версией форматируемого значения, ii и j j отслеживают положение в loc.grouping и число цифр в текущей группе, которые были преобразованы, соответственно7. quote_f lag равен true, когда в спецификации преобразования был отмечен символ'. Цикл do-while генерирует символы цифр в обратном порядке, заполняя буфер с конца к началу. Каждая цифра создается в строке 11. Затем строка 25 делится на 10 путем смещения значения вправо на одну десятичную цифру. Нас интересуют строки 12-24. Эта работа осуществляется только на системе, поддерживающей локали, на что указывает наличие заголовочного файла <locale.h>. Именованная константа HAVE_LOCALE в такой системе будет равна true8. Когда условие в строке 13 истинно, настало время добавить символ разделителя тысяч. Это условие можно прочесть как «если требуется группировка и текущее положение в loc .grouping указывает нужное для группировки количество и текущее число цифр равно группируемому количеству». Если это условие истинно, строка 14 добавляет символ разделителя тысяч. Комментарий обращает внимание на предположение, которое, вероятно, истинно, но которое может вновь появиться позже. ('XXX' является традиционным способом выделения опасного или сомнительного кода. Его»легко отыскать, и он весьма заметен для читателя кода.) Нам, вероятно, следовало выбрать более осмысленные имена вместо простых ii и jj, поскольку использующий их код короткий, отсутствие у нас воображения не представляет значительной проблемы. - Примеч. автора. Это устанавливается механизмом Autoconf и Automake. Autoconf и Automake являются мощными программными наборами, дающими возможность поддержки широкого круга Unix-систем систематическим образом. - Примеч. автора.
476 Глава 13. Интернационализация и локализация После использования текущего положения в loc .grouping строки 15-22 заглядывают в значение в следующем положении. Если это 0, продолжает использоваться значение текущего положения. Мы указываем на это, восстанавливая 0 в j j (строка 16). С другой стороны, если в следующем положении CHAR__MAX, фуппировка должна быть прекращена, и строка 18 убирает ее, устанавливая quote_f lag в false. В противном случае, следующее значение является значением группировки, поэтому строка 20 восстанавливает 0 в j j, а строка 21 увеличивает значение ii. Это низкоуровневый, подробный код. Однако, поняв один раз, как представляется информация в struct lconv, код читать просто (и его было просто писать). 13.2.7. Форматирование значений даты и времени: с time () иstrftime() В разделе 6.1 «Времена и даты» описаны функции для получения и форматирования значений времени и даты. Функция strf time () также может использовать локаль, если setlocale () была вызвана должным образом. Это демонстрирует следующая простая программа, chl3-times .с: /* chl3-times.с --- демонстрация времени на основе локали */ #include <stdio.h> #include <locale.h> #include <time.h> int main(void) { char buf[100]; time__t now; struct tm *curtime; setlocale(LC_ALL, ""); time(& now); curtime = localtime(& now); (void) strftime(buf, sizeof buf, "It is now %A, %B %d, %Y, %I:%M %p" , curtime); printf("%s\n", buf); printf("ctime() says: %s", ctime (& now)); exit@); } При запуске программы мы видим, что результаты strf time () в самом деле варьируют, тогда как результаты ctime () - нет: $ LC_ALL=en__US chl3-times /* Время в Соединенных Штатах */ It is now Friday, July 11, 2003, 10:35 AM ctime() says: Fri Jul 11 10:35:55 2003
13.2. Локали и библиотека С 477 $ LC_ALL=it_IT chl3-times /* Время в Италии */ It is now venerdi, luglio 11, 2003, 10:36 ctimeO says: Fri Jul 11 10:36:00 2003 $ LC_ALL=fr_FR chl3-times /* Время во Франции */ It is now vendredi, juillet 11, 2003, 10:36 ctimeO says: Fri Jul 11 10:36:05 2003 Причина отсутствия изменений в том, что с time () (и asctime (), на которой основана ctimeO) является традиционным интерфейсом; он существует для поддержки старого кода, strf time (), будучи более новым интерфейсом (первоначально разработанным для С89), свободен использовать локали. 13.2.8. Другие данные локали: nl_langinfо () Хотя ранее мы сказали, что API catgets () трудно использовать, одна часть этого API обычно полезна: nl_langinf о (). Она предоставляет дополнительные связанные с локалью сведения, помимо тех, которые доступны из struct lconv: #include <nl_types.h> #include <langinfo.h> char *nl_langinfo (nl__item item); Заголовочный файл <nl_types . h> определяет тип nl_i tern. (Это скорее всего int или enum.) Параметр item является одной из именованных констант, определенных в <langinf о ,h>. Возвращаемое значение является строкой, которую можно при необходимости использовать либо непосредственно, либо в качестве форматирующей строки для strf time (). Доступная информация поступает из нескольких категорий локали. В табл. 13.3 перечислены константы элементов, соответствующие категории локали и их значения. Эра является определенным временем в истории. Поскольку она иМеет отношение к датам и временам, она имеет наибольший смысл в странах, управляемых императорами и династиями9. Спецификации эр POSIX могут определять эры ранее 1 г. н.э. В таких случаях у начальной даты большее абсолютное числовое значение, чем у конечной даты. Например, Александр Великий правил с 336 г. до н.э. по 323 г. до н.э. Значение, возвращенное 'ln_langinf о (ERA)', если оно не равно NULL, состоит из одной или более спецификаций эр. Каждая спецификация отделена от следующей символом *;'. Компоненты спецификации каждой эры отделяются друг от друга символом ':'. Компоненты описаны в табл. 13.4. Хотя американцы часто ссыпаются на эры определенных президентов, они не являются частью национального календаря в том же смысле, как в Японии до Второй мировой войны или в докоммунистическом Китае. - Примеч. автора.
478 Глава 13. Интернационализация и локализация Таблица 13.3. Значения элементов для nl__langinf о () Элемент Категория Значение ABDAY_1, .. ABDAY__7 ABMON_l, .. ABMON_12 ALT_DIGITS LCJTIME LC_TIME LC_TIME AM_STR, PM_STR LC_TIME CODESET LC TYPE CRNCYSTR DAY_1, DAY_7 D_FMT D_T_FMT ERA_D_FMT ERA_D_T_FMT ERA__T__FMT ERA MON_l„ MON_12 RADIXCHAR THOUSEP T_FMT_AMPM TJFMT YESEXPR, NOEXPR LC_MONETARY LC_TIME LC_TIME LC_TIME LCJTIME LC_TIME LC_TIME LC__TIME LC_TIME LC_NUMERIC LC__NUMERIC LC_TIME LC__TIME LC_MESSAGES Сокращенные названия дней недели. Воскресенье является днем 1. Сокращенные названия месяцев. Альтернативные символы для цифр; см. текст. Обозначения a.m./p.m. для локали. Имя кодовой страницы для локали; т. е. использующиеся набор символов и кодировка. Символ местной валюты, описанный ниже. Названия дней недели. Воскресенье является днем 1. Формат даты. Формат даты и времени. Формат даты эры. Формат даты и времени эры. Формат времени эры. Сегменты описания эры; см. текст. Названия месяцев. Символ системы счисления. Для базы 10 это символ точки в десятичной дроби. Символ-разделитель тысяч. Формат времени в записи a.m./p.m. Формат времени. Строка, представляющая положительный и отрицательный ответы.
13.2. Локали и библиотека С 479 Таблица 13.4. Компоненты спецификации эры Компонент Значение Направление Символы '+' или '-'. '+' означает, что эра отсчитывается от численно меньшего года к численно большему году, а '-' означает обратный порядок. Смещение Ближайший к дате начала эры год. Дата начала Дата начала эры в виде *гггг/мм/дд\ Это соответственно год, месяц и день. Годы до н.э. используют для гггг отрицательные значения. Дата конца Дата завершения эры в том же самом виде. Допустимы два дополнительных вида: -* означает «начало времени», а+* означает «конец времени». Название эры Название эры, соответствующее спецификации преобразования %ЕС функции strf time (). Формат эры Формат года в пределах эры, соответствующий спецификации преобразования %EY функции strf time (). Значение ALT_DIGITS также нуждается в некотором объяснении. Некоторые локали предоставляют «альтернативные цифры». (Рассмотрите арабский язык, в котором используется десятичная система счисления, но изображения для цифр 0-9 другие. Или рассмотрите гипотетическую локаль «Древнего Рима», использующую римские цифры.) Они появляются, например, в различных спецификациях преобразования %Ос в функции strf time (). Возвращаемое значение для 4nl_langinf о (ALT__DIGITS)' является разделяемым точками с запятой списком строк символов для альтернативных цифр. Первая должна использоваться для 0, следующая для 1 и т. д. POSIX утверждает, что могут быть предоставлены до 100 альтернативных символов. Сущность в том, чтобы избежать ограничения л окал ей использованием символов цифр ASCII, когда у локали есть собственная система счисления. Наконец, 'nl__langinfo (CRNCYSTR)' возвращает символ местной валюты. Первый символ возвращаемого значения, если это '-', '+' или '.', указывает, как должен использоваться символ: Символ должен быть перед значением. + Символ должен быть после значения. Символ должен заменить символ основания (разделитель десятичной дроби).
480 Глава 13. Интернационализация и локализация 13.3. Динамический перевод сообщений программ Только что освещенные интерфейсы стандартной библиотеки С решают простые части проблемы локализации. Для денежных, числовых значений, значений времени и даты, также, как для проблем сортировки строк, применяется управление посредством таблиц специфичных для локали данных (таких, как списки названий месяцев и дней). Однако, большая часть взаимодействия пользователя с текстовой программой осуществляется в виде выводимых сообщений, таких, как приглашения или сообщения об ошибках. Проблема заключается в необходимости избежания множества версий одной и той же программы, которые отличаются лишь содержанием строк сообщений. Решением де-факто в мире GNU является GNU get text. (GNU программы сталкиваются с подобными проблемами с элементами меню; обычно у каждого большого инструментария пользовательского интерфейса свой способ решения этой проблемы.) GNU get text дает возможность перевода сообщений программы на другие языки во время исполнения. Внутри кода программы этот перевод включает несколько шагов, каждый из которых использует свои библиотечные функции. Когда сама программа должным образом подготовлена, несколько утилит на уровне оболочки дают возможность подготовить переводы на другие языки. Каждый такой перевод называется списком сообщений (message catalog). 13.3.1. Установка текстового домена: textdomain () Законченное приложение может содержать множество компонентов: отдельные исполняемые файлы, написанные на С или C++ или на языках сценариев, которые также могут получить доступ к возможностям gettext, таких, как gawk или оболочка Bash. Все компоненты приложения разделяют один и тот же текстовый домен, который является строкой, уникально идентифицирующей приложение. (Примерами могут быть «gawk» или «coreutils»; первое является простой программой, а последнее- целым набором программ.) Текстовый домен устанавливается функцией textdomain (): #include <libintl.h> /* GLIBC */ char * textdomain (const char *domainname) ;• Каждый компонент должен вызывать эту функцию со строкой, указывающей на текстовый домен, в составе первоначальной инициализации в main (). Возвращаемое значение является текущим текстовым доменом. Если аргумент domainname равен NULL, возвращается текущий домен; в противном случае, он устанавливается в указанное значение, а последнее возвращается. Возвращаемое значение NULL указывает на какую-нибудь разновидность ошибки. Если текстовый, домен не установлен с помощью textdomain (), по умолчанию используется «messages».
13.3. Динамический перевод сообщений программ 481 13.3.2. Перевод сообщений: gettext () Следующим после установки текстового домена шагом является использование функции gettext () (или ее разновидности) для каждой строки, которая должна быть переведена. Несколько функций предоставляют службы перевода: #include <libintl.h> * /* GLIBC ¦*/ char *gettext(const char *msgid); char *dgettext(const char *domainname/ const char *msgid); char *dcgettext(const char *domainname, const char *msgid, int category); Аргументы, используемые в этих функциях, следующие: const char *msgid Переводимая строка. Она действует в качестве ключа к базе данных переводов. const char *domainname Текстовый домен, из которого нужно получить перевод. Таким образом, хотя main () вызвала textdomain () для установки собственного домена приложения, сообщения могут быть получены из других текстовых доменов. (Это наиболее применимо к сообщениям, которые могли бы быть, например, в текстовом домене библиотеки от третьей стороны.) int category Одна из описанных ранее категорий доменов (LC_TIME и т.п.). Доменом по умолчанию является то, что было раньше установлено с помощью textdomain () («messages», если textdomain () никогда не вызывалась). Категорией по умолчанию является LC_MESSAGES. Предположим, main () делает следующий вызов: textdomain("killerapp"); Тогда 'gettext ("my message")' эквивалентно 'dgettext ("killerapp", "my message")'. Обе функции, в свою очередь, эквивалентны 'dcgettext ("killerapp", "my message", LC_MESSAGES)\ В 99,9% времени бывает нужно использовать gettext (). Однако, другие функции обеспечивают гибкость при работе с другими текстовыми доменами или категориями локалей. Скорее всего, эта гибкость потребуется при программировании библиотек, поскольку автономная библиотека почти наверняка будет использовать свой собственный текстовый домен. Все функции возвращают строки. Строка является либо переводом данного msgid, либо, если перевода не существует, первоначальной строкой. Таким образом, всегда имеется какой-нибудь вывод, даже если это первоначальное сообщение (предположительно на английском). Например: /* Каноническая первая программа, локализованная версия. */ #include <stdio.h> #include <locale.h> #include <libintl.h> int main(void) { setlocale(LC_ALL, ""); 16-159
482 Глава 13. Интернационализация и локализация printf("%s\n", gettext("hello, world")); return 0; } Хотя сообщение является простой строкой, мы не используем ее непосредственно в форматирующей строке printf (), поскольку в общем перевод может содержать символы %. Вскоре, в разделе 13.3.4 «Упрощение использования gettext О», мы увидим, как облегчить использование gettext () в крупномасштабных, реальных программах. 13.3.3. Работа с множественными числами: ngettext () Перевод во множественном числе доставляет дополнительные трудности. Простой код мог бы выглядеть примерно так: printf("%d word%s misspelled\n", nwords, nwords > 1 ? "s" : ""); /* или */ printf("%d %s misspelled\n", nwords, nwords == 1 ? "word" : "words"). Это подходит для английского языка, но перевод становится трудным. Во-первых, во многих языках множественное число не образуется с такой же легкостью, как в английском (добавлением суффикса s для большинства слов). Во-вторых, во многих языках, особенно в Восточной Европе, имеются несколько форм множественного числа, каждая из которых указывает на то, сколько объектов обозначает форма. Соответственно даже код наподобие следующего не будет достаточным: if (nwords ==1) printf("one word misspelled\n"); else printf("%d words misspelled\n", nwords) Решением является параллельный набор процедур специально для форматирования множественных значений: #include <libintl.h> . /* GLIBC */ char *ngettext(const Char *msgid, const char *msgid_plural, unsigned long int n) ; char *dngettext(const char *domainname, const char *msgid, const char *msgid__plural, unsigned long int n); char *dcngettext(const char *domainname, const char *msgid, const char *msgid_plural, unsigned long int n, int category) Помимо первоначального аргумента msgid, эти функции принимают дополнительные аргументы: const char *msgid__plural Строка по умолчанию для использования в качестве множественного числа. Вскоре будут примеры. unsigned long int n Число имеющихся элементов.
13.3. Динамический перевод сообщений программ 483 — ~ / " Список сообщений каждой локали указывает, как переводить множественные числа10. Функция ngettextO (и ее варианты) проверяет п и на основании спецификации в списке сообщений возвращает соответствующий перевод msgM. Если в списке нет перевода для msgid, или находясь в локали «С», ngettext () возвращает msgid, если 'п == 1'; в противном случае она возвращает msgid_plural. Таким образом, наш пример ошибочных слов выглядит следующим образом: printf("%s\n", ngettext("%d word misspelled", "%d words misspelled", nwords), nwords); Обратите внимание, что nwords должен быть передан ngettext () для выбора форматирующей строки, а затем printf О для форматирования. Вдобавок, будьте осмотрительны и не используйте макрос или выражение, значение которого каждый раз изменяется, как в случае 'п++'! Такое может случиться, если вы осуществляете глобальное редактирование, добавляя вызовы ngettext () и не обращая на это внимания. 13.3.4. Упрощение использования gettext () Вызов gettext () в исходном коде программы служит двум целям. Во-первых, он осуществляет перевод во время исполнения, что является в конце концов главным. Однако, он служит также для отметки строк, которые нужно перевести. Утилита xgettext читает исходный код программы и извлекает все оригинальные строки, которые нужно перевести. (Далее в главе мы кратко рассмотрим это.) Рассмотрим все-таки случай, когда статические строки не используются непосредственно: static char *copyrights[] = { "Copyright 2004, Jane Programmer", "Permission is granted . ..", /* ... Здесь куча легальностей */ NULL }; void copyright(void) { int i ; for (i = 0; copyrights[i] != NULL, i++) printf("%s\n", gettext(copyrights[i] )) ; } Здесь мы хотели бы иметь возможность вывести переводы строк об авторских правах, если они доступны. Однако, как извлекающее устройство xgettext предполагает найти эти строки? Мы не можем заключить их в вызовы gettext (), поскольку это не будет работать во время компиляции: Подробности приведены в документации GNU gettext. Здесь мы концентрируемся на потребностях разработчика, а не переводчика. - Примеч. автора.
484 Глава 13. Интернационализация и локализация /* ПЛОХОЙ КОД: не будет компилироваться */ static char *copyrights[] = { gettext("Copyright 2004, Jane Programmer")/ gettext("Permission is granted ..."), /* ... Здесь куча легальностей */ NULL }; 13.3.4.1. Переносимые программы: "де ttext'.h" Здесь мы предполагаем, что вы хотите написать программу, которая может использоваться вместе с библиотекой GNU gettext на любой системе Unix, а не только GNU/ Linux. Следующий раздел описывает, что сделать для программ только для GNU/Linux. Пометка строк включает два шага. Первый заключается в использовании вспомогательного заголовка gettext .h, который поставляется с дистрибутивом GNU gettext. Этот файл обрабатывает несколько проблем переносимости и компиляции, упрощая использование gettext () в ваших собственных программах: #define ENABLEJSILS 1 /* ENABLE_NLS должен быть true, чтобы gettext() работала */ #include "gettext.h" /* Вместо <libintl.h> */ Если макрос ENABLE__NLS не определен11 или установлен в ноль, gettext .h развертывает вызовы gettext () в первый аргумент. Это делает возможным перенос кода, использующего gettext (), на системы, в которых не установлены ни GNU gettext, ни собственная их версия. Помимо прочего, этот заголовочный файл определяет следующий макрос: /* Вызов псевдофункции, который служит.в качестве маркера для автоматического извлечения сообщений, но не осуществляющий вызов gettext(). Перевод времени исполнения осуществляется в другом месте кода. Аргумент String должен быть символической строкой. Сцепленные строки и другие строковые выражения не будут работать. Разворачивание макроса не параметризовано, так что он подходит для инициализации статических переменных 'chart]' или 'const char[]'. */ #define gettext_noop(String) String Комментарий самодостаточен. С помощью этого макроса мы можем теперь перейти ко второму шагу. Мы перепишем код следующим образом: #define ENABLE_NLS l #include "gettext.h" static char copyrights!] = gettext_noop("Copyright 2004, Jane Programmer\n" "Permission is granted ...\n" •/* ... Здесь куча легальностей */ 11 Этот макрос обычно определяется автоматически программой configure, либо в специальном заголовке, либо в командной строке компилятора, configure создается с помощью Autoconf и Automake. - Примеч. автора.
13.3. Динамический перевод сообщений программ 485 "So there. ") ; void copyright(void) { ' ' .. printf ("%s\n", gettext (copyrights).) ; } Обратите внимание, что мы сделали два изменения. Во-первых, copyrights теперь является одной длинной строкой, созданной с использованием возможности конкатенации строк стандартного С. Эта простая строка затем включена в вызов gettext__noop (). Нам нужна одна строка, чтобы легальности могли быть переведены в виде одного элемента. Второе изменение заключается в непосредственном выводе перевода в виде одной строки в copyright (). К этому времени вы, возможно, думаете: «Вот здорово, набирать каждый раз 'gettext (...)' довольно неприятно». Ну, вы правы. Это не только создает лишнюю работу по набиванию, но также и затрудняет чтение исходного кода. Соответственно, когда вы используете заголовочный файл gettext .h, руководство GNU gettext рекомендует включить два других макроса с именами _ () и N_ () следующим образом: #define ENABLE__NLS l #include "gettext.h" #define _(msgid) gettext(msgid) #define N_(msgid) msgid Такой подход снижает накладные расходы по использованию gettext () всего лишь тремя дополнительными символами для переводимой строковой константы и всего лишь четырьмя символами для статических строк: #include <stdio.h> #define ENABLE_NLS 1 #include "gettext.h" #define _(msgid) gettext(msgid) #define N_(msgid) msgid static char copyrights[] = N_("Copyright 2004, Jane Programmer\n" "Permission is granted ...\n" /* ... Здесь куча легальностей */ "So there. ") ; void copyright(void) { printf("%s\n", gettext(copyrights)); } int main(void) {
486 Глава 13. Интернационализация и локализация setlocale(LC_ALL, ""); /* gettext.h gets <locale.h> for us too */ printf("%s\n", .("hello, world")); copyright(); exit@); } Эти макросы скромны, и на практике все GNU программы, использующие GNU gettext, следуют этому соглашению. Если вы собираетесь использовать GNU gettext, вам тоже нужно следовать этому соглашению. 13.3.4.2. ТолькоGUBC: <libintl. h> Для программ, которые будут использоваться лишь на системах с GLIBC, использование заголовочных файлов и макросов похоже, но проще: #include <stdio.h> #include <libintl.h> #define _(msgid) gettext(msgid) #define N_(msgid) msgid /* ... все остальное то же ... */ Как мы видели ранее, заголовочный файл <libintl.h> объявляет gettext () и другие функции. Вам все равно нужно определять ^.0 и N_(), но не нужно беспокоиться о ENABLE_NLS или включении с исходным кодом вашей программы файла gettext.h. 13.3.5. Перестановка порядка слов с помощью printf () Иногда при переводах порядок слов, естественный для английского языка, не подходит в других языках. Например, на английском прилагательные идут перед определяемыми существительными, а на многих других языках - после. Таким образом, следующий код представляет проблему: char *animal_color, *animal; if (...) { animal_color = _("brown"); animal = „("cat"); } else if (...) { } else { } printf (_('"fhe %s %s looks at you enquiringly. \n") , animal_color, color); Здесь форматирующая строка, animal__color и animal неудачно включены в вызов gettext (). Однако, после перевода утверждение будет неверным, поскольку порядок аргументов не может быть изменен во время исполнения.
13.3. Динамический перевод сообщений программ 487 Чтобы обойти это, версия семейства printf О POSIX (но не ISO С) допускает использовать в описателе формата указатель положения. Он принимает форму десятичного числа, за которым следует символ $, сразу после начального символа %. Например: printf("%2$s, %l$\n", "world", "hello"); Указатель положения обозначает аргумент из списка, который следует использовать; отсчет начинается с 1 и не включает саму форматирующую строку. Этот пример выводит знаменитое сообщение 'hello, world' в правильном порядке. GLIBC и Solaris реализуют эту возможность. Поскольку это часть POSIX, если printf () вашего поставщика Unix не реализует ее, она вскоре должна появиться. За указателем положения могут следовать любые обычные флаги printf (), указатели ширины полей и точности. Вот правила для использования указателей положения: Форма с указателем положения не может смешиваться с формой без нее. Другими словами, или каждый указатель формата включает указатель положения, или ни один его не включает. Конечно, %% может использоваться всегда. Если в форматирующей строке используется N-й аргумент, в этой строке должны использоваться также все аргументы до N. Соответственно, следующее неверно: pritnf("%3$s %l$s\n", "hello", "cruel", "world"); • Ссылка на определенный аргумент может быть сделана указателем положения несколько раз. Не позиционные спецификаторы формата всегда движутся через список аргументов последовательно. Эта возможность не предназначена для непосредственного использования программистами приложений, она скорее для переводчиков. Например, перевод предыдущей форматирующей строки, "The %s %s looks at you enquiringly. \n", на французский мог бы быть: "Le %2$s %l$s te regarde d'un aire interrogateur.\n" (Даже этот перевод не совершенен: артикль «Le» имеет род. Подготовка программы к переводу трудная задача!) 13.3.6. Тестирование переводов в персональном каталоге Коллекция сообщений в программе называется списком сообщений (message catalog). Этот термин применяется также к каждому из переводов сообщений на другой язык. Когда программа установлена, каждый перевод также устанавливается в стандартное место, где gettext () может во время исполнения найти нужный перевод. Может оказаться полезным разместить переводы не в стандартном, а в другом каталоге, особенно для тестирования программы. Особенно на больших системах, у обычного пользователя может не быть необходимых разрешений для установки файлов в системные каталоги. Функция bindtextdomain () дает gettext () альтернативное место для поиска переводов: #include <libintl.h> /* GLIBC' */ char *bindtextdoinain (const char *domainname/ const char *dirname);
488 Глава 13. Интернационализация и локализация Полезные каталоги включают 4.' для текущего каталога и /tmp. Может оказаться удобным также получить каталог из переменной окружения, подобно этому: char *td_dir; setlocale(LC_ALL, ""); textdomain("killerapp"); if ((td__dir = getenv("KILLERAPP_TD_DIR" ) ) != NULL) bindtextdomain ("killerapp" , td__dir) ; bindtextdomain () должна быть вызвана до вызовов любой из функций из семейства gettext (). Мы увидим пример ее использования в разделе 13.3.8 «Создание переводов». 13.3.7. Подготовка интернационализированных программ К настоящему моменту мы рассмотрели все компоненты, из которых состоит интернационализированная программа. Данный раздел подводит итоги. 1. Включите в свое приложение заголовочный файл gettext .h, добавьте определения для макросов _() и N__() в заголовочный файл, который включается во все ваши исходные файлы на С. Не забудьте определить именованную константу ENABLE_NLS. 2. Вызовите соответствующим образом setlocale(). Проще всего вызвать 'setlocale (LC_ALL, "")•', но иногда приложению может потребоваться быть более разборчивым в отношении используемых категорий локали. 3. Выберите для приложения текстовый домен и установите его с помощью textdomain (). 4. При тестировании свяжите текстовый домен с определенным каталогом при помощи bindtextdomain(). 5. Используйте соответствующим образом strfmon(), strftimeO и флаг '. Если нужна другая информация о локали, используйте nl__langinf о (), особенно в сочетании сstrftime(). 6. Пометьте все строки, которые должны быть переведены, соответствующими вызовами _ () или N_ (). Хотя некоторые не следует так помечать. Например, если вы используете getopt_long () (см. раздел 2.1.2 «Длинные опции GNU»), вы, вероятно, не захотите, чтобы имена длинных опций были помечены для перевода. Не требуют перевода и простые форматирующие строки наподобие "%d %d\n", также как отладочные сообщения. 7. В нужных местах используйте ngettext () (или ее варианты) для значений, которые могут быть 1 или больше 1. 8. Упростите жизнь для своих переводчиков, используя строки с полными предложениями вместо замены слов с помощью %s и ? :. Например: if (/* возникла ошибка */) { /* ВЕРНО *-/ /* Использовать несколько строк для упрощения перевода. */ •if (input_type == INPUT_FILE) fprintf(stderr, _("%s: cannot read file: %s\n"), argv[0], strerror(errno)); else
13.3. Динамический перевод сообщений программ 489' fprintf(stderr, _("%s: cannot read pipe: %s\n"), argv[0], strerror(errno)); } Это лучше, чем if (/* возникла ошибка */) { /* НЕВЕРНО */ fprintf(stderr, _("%s: cannot read %s: %s\n")/ argv[0], input_type == INPUT_FILE ? „("file'.' ) : _("pipe"), strerror(errno)); } Как только что показано, хорошей мыслью является включение комментария, сообщающего о намеренном использовании нескольких строк, чтобы упростить перевод сообщений. 13.3.8. Создание переводов После интернационализации программы необходимо подготовить переводы. Это осуществляется с помощью нескольких инструментов уровня оболочки. Мы начнем с интернационализированной версии ch06-echodate.c из раздела 6.1.4 «Преобразование разложенного времени в time_t»: /* chl3-echodate.с демонстрация переводов */ #include <stdio..h> #include <time.h> #include <locale.h> . | #define ENABLE_NLS 1 #include "gettext.h" #define _(msgid) gettext(msgid) #define N_(msgid) msgid int main(void) { struct tm tiru time_t then; setlocale(LC_ALL, ""); bindtextdomain("echodate","."); textdomain("echodate") ; printf("%s" , „("Enter a Date/time as YYYY/MM/DD HH:MM:SS : ")); scanf("%d/%d/%d %d:%d:%dn, Sc tm.tm_year, & tm.tm_mon/ & tm.tm_mday/ & tm.tm_hour, & tm.tm._min, & tm.tm_sec); /* Проверка ошибок для краткости опущена. */ tm.tm_year -= 1900; tm.tm_mon -= 1;
490 Глава 13. Интернационализация и локализация tm.tm_isdst = -1; /* О летнем времени ничего не известно */ then = mktime(& tm); printf(_("Got: %s")/ ctime(& then)); exit@); } Мы намеренно использовали "gettext .h", а не <gettext .h>. Если наше приложение поставляется с отдельной копией библиотеки gettext, тогда "gettext.h" найдет ее, избежав использования системной копии. С другой стороны, если имеется лишь системная копия, она будет найдена, если локальной копии нет. ОбщеизЁестно, что ситуация усложнена фактом наличия на системах Solaris библиотеки gettext, которая не имеет всех возможностей версии GNU. Переходя к созданию переводов, первым шагом является извлечение переводимых строк. Это осуществляется программой xgettext: $ xgettext --keyword=_ --keyword=N_ \ > --default-domain»echodate chl3-echodate.c Опции --keyword сообщает xgettext, что нужно искать макросы _() и N_(). Программа уже знает, как извлекать строки из gettext () и ее вариантов, а также из gettext_noop(). Вывод xgettext называется переносимым объектным файлом. Имя файла по умолчанию messages.ро, что соответствует текстовому домену по умолчанию "messages". Опция --default-domain обозначает текстовый домен для использования в имени выходного файла. В данном случае, файл назван echodate. ро. Вот его содержание: # SOME DESCRIPTIVE TITLE. /* Шаблон, нужно отредактировать */ # Copyright (С) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" /* Подробная информация */ . msgstr "" /* Заполняет каждый переводчик */ "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2003-07-14 18:46-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: chl3-echodate.с:19 /* Местоположение сообщения */
13.3. Динамический перевод сообщений программ 491 msgid "Enter a Date/time as YYYY/MM/DD HH:MM:SS : " /* Оригинальное сообщение */ msgstr "" /* Здесь перевод */ #: chl3-echodate.c:32 /* To же самое для каждого сообщения ¦*/ #, c-format msgid "Got: %s" msgstr "" Этот первоначальный файл используется повторно для каждого перевода. Таким образом, это шаблон для переводов, и по соглашению, для отображения этого факта он должен быть переименован с расширением . pot {portable object template - переносимый объектный шаблон): $ znv echodate.po echodate.pot He владея свободно несколькими языками, мы решили перевести сообщения на свинский латинский. Следующим шагом является создание перевода. Это осуществляется копированием файла шаблона и добавлением к новой копии перевода: $ ср echodate.pot piglat.po $ vi piglat.po /* Добавить переводы, используя любимый редактор */ Имя по соглашению должно быть язык, ро, где язык является стандартным международным сокращением из двух или трех букв для обозначения языка. Иногда используется форма язык_страна .ро: например, pt_BR.po для португальского в Бразилии. Поскольку свинский латинский не является настоящим языком, мы назвали файл piglat. ро12. Вот содержание после добавления перевода: # echodate translations into pig Latin # Copyright (C) 2004 Prentice-Hall # This file is distributed under the same license as the echodate package. # Arnold Robbins <arnold@exainple.com> 2004 # , #, fuzzy msgid "" msgstr "" "Project-Id-Version: echodate 1.0\n" "Report-Msgid-Bugs-To: arnold@example.com\n" "POT-Creation-Date: 2003-07-14 18:46-0700\n" "PO-Revision-Date: 2003-07-14 19:00+8\n" "Last-Translator: Arnold Robbins <arnold@example.com>\n" "Language-Team: Pig Latin <piglat@li.example.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ASCII\n" "Content-Transfer-Encoding: 8bit\n" Pig - свинья, поросенок (англ.) - Примеч. перев.
492 Глава 13. Интернационализация и локализация #: chl3-echodate.c:19 msgid "Enter a Date/time as YYYY/MM/DD HH:MM:SS : " msgstr "Enteray A Ateday/imetay asay YYYY/MM/DD HH:MM:SS : " #: chl3-echodate.с:32 #, с-format msgid "Got: %s" msgstr "Otgay: %s" Хотя можно было бы произвести линейный поиск в переносимом объектном файле, такой поиск был бы медленным. Например, в gawk имеется примерно 350 отдельных сообщений, а в GNU Coreutils - свыше 670. Линейный поиск в файле с сотнями сообщений был бы заметно медленным. Поэтому GNU gettext использует для быстрого поиска сообщений двоичный формат. Сравнение осуществляет msgf mt, выдавая объектный файл сообщений: $ msgfmt piglat.po -о piglat.mo При сопровождении программы изменяются строки, используемые программой: добавляются новые, другие удаляются или изменяются. По крайней мере, может измениться положение строки в исходном файле. Таким образом, файлы переводов .ро, вероятно, устареют. Программа msgmerge объединяет старые файлы переводов е новым файлом .pot. Затем результат может быть обновлен. Этот пример выполняет объединение и повторное компилирование: $ msgmerge piglat.po echodate.pot -о piglat.new.po /* Объединить файлы */ $ mv piglat.new.po piglat.po /* Переименовать результат */ $ vi piglat.po /* Модернизировать перевод */ $ msgfmt piglat.po -o piglat.mo /* Восстановить файл .mo */ Откомпилированные файлы .mo помещаются в файл base/locale/category/, textdomain.то. На системах GNU/Linux base является /usr/share/locale. locale является обозначением языка, например, *es\ 'f г' и т. д. category является категорией локали; для сообщений это LC_MESSAGES. textdomain является текстовым доменом программы: в нашем случае это echodate. В качестве реального примера в /usr/share/locale/es/LC_MESSAGES/coreutils .mo находится перевод GNU Coreutils на испанский. Функция bindtextdomain() изменяет в местоположении часть base. В chl3 -echodate . с мы меняем ее на *.'. Таким образом, нужно создать соответствующие каталоги и поместить туда перевод на свинский латинский: $ mkdir -p en_US/LC_MESSAGES /* Нужно использовать реальную локаль */ $ qp piglat.mo en_US/LC_MESSAGES/echodate.mo /* Поместить файл в нужное место */ Должна использоваться реальная локаль13; мы «притворяемся» использующими "erOJS". Разместив перевод, устанавливаем соответствующим, образом LC_ALL, скрещиваем пальцы и запускаем программу: , Мы тщетно потратили 30 или 45 минут, пытаясь использовать каталог piglat /LC_MESSAGES и установку 'LC_ALL=piglat' без всякого успеха, пока не выяснили это. - Примеч. автора.
13.4. Не могли бы вы произнести это для меня по буквам? 493' $ LC__ALL=en__US chl3-echodate /* Запуск программы */ Enteray A Ateday/imetay asay YYYY/MM/DD HH:MM:SS : 2003/07/14 21:19:26 Otgay: Mon Jul 14 21:19:26 2003 Последнюю версию GNU get text можно найти в каталоге дистрибутива GNU gettext,4. Этот раздел лишь слегка коснулся поверхности процесса локализации. GNU gettext предоставляет множество инструментов для работы с переводами, и в особенности для облегчения поддержания современности переводов по мере развития исходного кода программы. Процесс ручного обновления переводов осуществим, но утомителен. Эта задача легко автоматизируется с помощью make; в частности, GNU gettext хорошо интегрируется для обеспечения этой возможности с Autoconf и Automake, снимая с программиста значительный груз по разработке. Рекомендуем прочесть документацию GNU gettext, чтобы больше узнать как об этих проблемах в частности, так и о GNU gettext в общем. 13.4. Не могли бы вы произнести это для меня по буквам? В самые ранние дни вычислительной техники различные системы назначали различные соответствия между числовыми значениями и глифами (glyphs) - символами, такими, как буквы, цифры и знаки пунктуации, используемыми для общения с людьми. В конечном счете появились два широко использующихся стандарта: кодировка EBCDIC, используемая IBM и подобными ей мэйнфреймами, и ASCII, использующаяся для всего остального. Сегодня, за исключением мэйнфреймов, ASCII является основой для всех других использующихся наборов символов. Оригинальный семиразрядный набор символов ASCII достаточен для американского английского и большинства знаков пунктуации и специальных символов (таких, как $, но нет символа для «цента»). Однако, имеется много языков и много стран, которым нужны другие наборы символов. ASCII не оперирует версиями романских символов с надстрочными значками, использующимися в Европе, а во многих азиатских языках тысячи символов. Для устранения этих недостатков были разработаны новые технологии. Литература по интернационализации изобилует ссылками на три фундаментальных термина. Определив их и взаимоотношения между ними, мы сможем представить общее описание соответствующих функций API С. Набор символов (character set) Определение значений, присваиваемых различным целым величинам; например того, что А равно 65. Любой набор символов, использующий более восьми битов на символ, называется многобайтным набором символов. ftp: / / ftp. gnu. org/gnu/gettext - Примеч. автора.
494 Глава 13. Интернационализация и локализация Представление набора символов (character set encoding) ASCII использует для представления символов один байт. Таким образом, целое значение хранится само по себе, непосредственно в дисковых файлах. Более современные наборы символов, особенно различные версий Unicode15, используют для представления символов 16-разрядные или даже 32-разрядные целые значения. Для большинства определенных символов один, два или даже три старших байта целого значения равны нулю, что делает непосредственное хранение таких значений на диске неэффективным. Представление набора символов описывает механизм для преобразования 16- или 32-разрядных значений в последовательности от одного до шести байтов для сохранения на диске таким образом, что в целом наблюдается значительная экономия дисковой памяти. Язык Правила данного языка определяют использование набора символов. В частности, правила влияют на сортировку символов. Например, на французском е, ё и ё все должны находиться между d и f, независимо от назначенных этим символам числовых значений. Различные языки могут назначить (и назначают) одним и тем же глифам различный порядок сортировки. Со временем для поддержки многобайтных наборов символов были разработаны различные технологии. Вычислительная практика медленно стремится к Unicode и соответствующим его представлениям, но стандартный С и POSIX поддерживают как старые, так и современные технологии. В данном разделе представлен концептуальный обзор различных возможностей. Нам самим не приходилось их использовать, поэтому мы предпочитаем просто познакомить с ними и предоставить ссылки на дополнительную информацию. 13.4.1. Широкие символы Мы начнем с концепции широких символов (wide character). Широкий символ является целым типом, в котором может храниться любое значение из определенного используемого многобайтного набора символов! Широкие символы представлены на С типом wchar_t. C99 предоставляет соответствующий тип wint_t, в котором может находиться любое значение, допустимое для wchar__t, а также специальное значение WEOF, аналогичное обычному EOF из <stdio. h>. В заголовочном файле <wchar. h> определены различные типы. Ряд функций, сходных с функциями в <ctype.h>, такие, как iswalnumO и др., определены в заголовочном файле <wctype. h>. Широкие символы могут быть от 16 до 32 битов размером в зависимости от реализации. Как упоминалось, они нацелены на манипулирование данными в памяти и обычно не хранятся в файлах непосредственно. http: / /www. Unicode. org - Примеч. автора.
13.4. Не могли бы вы произнести это для меня по буквам? 495 Стандарт С предусматривает для широких символов большое число функций и макросов, соответствующих традиционным функциям, работающим с данными char. Например, wprintf (), iswlower () и т. д. Они документированы в справочных страницах GNU/Linux и в книгах по стандартному С. 13.4.2. Представления многобайтных символов Строки широких символов сохраняются на диске путем преобразования их в памяти в многобайтное представление набора символов с последующей записью в дисковый файл. Сходным образом, такие строки считываются с диска через низкоуровневый блочный ввод/ вывод, а затем конвертируются в памяти из многобайтной версии в версию широких символов. Многие описанные кодировки используют для представления многобайтных символов состояния регистра (shift states). Другими словами, в данном потоке байтов значения байтов представляют самих себя до тех пор, пока не встретится специальное управляющее значение. В этот момент интерпретация изменяется в соответствии с текущим состоянием регистра. Таким образом, одно и то же восьмибитовое значение может иметь два значения: одно для обычного состояния, без использования регистра, и другое для использования регистра. Предполагается, что правильно закодированные строки начинаются и заканчиваются с одним и тем же состоянием регистра. Значительным преимуществом Unicode является то, что его представления являются самокорректирующимися; кодировки не используют состояния регистров, поэтому потеря данных в середине не может повредить последующим закодированным данным. Первоначальные версии функций преобразования многобайтных символов в широкие и широких в многобайтные поддерживали закрытую копию состояния преобразования (например, состояние регистра, а также все остальное, что могло понадобиться). Такая модель ограничивает использование функции лишь одним видом преобразования в течение жизни программы. Примерами являются mblenf) (определение длины многобайтной строки), mbtowc () (преобразование многобайтного символа в широкий), wctomb () (преобразование широкого символа в многобайтный), mbstowcsO (преобразование многобайтной строки в строку широких символов), wcstombs () (преобразование строки широких символов в многобайтную строку). Новые версии этих процедур называются повторно запускаемыми (restartable). Это означает, что код уровня пользователя сохраняет состояние преобразования в отдельном объекте типа mbstate_t. Соответствующими примерами являются mbrlenO, mbrtowc (), wcrtomb(), mbsrtowcs () и wcsrtornbs (). (Обратите внимание на г в их именах, это означает «restartable».)
496 Глава 13. Интернационализация и локализация 13.4.3. Языки Языковые проблемы управляются локалью. Ранее в главе мы уже видели setlocale (). POSIX предоставляет продуманный механизм для определения правил, посредством которых работает л окал ь; некоторые подробности см. в справочной странице GNU/Linux localeE), а полностью ^ в самом стандарте POSIX. Правда в том, что подробности на самом деле не нужны. Вам, как разработчику программ, не нужно беспокоиться о них; как заставить все работать, зависит от разработчиков библиотек. Все, что нужно, это понять концепции и использовать в своем коде соответствующие функции, такие, как strcolK) (см. раздел 13.2.3 «Сравнение строк: str- coll() и strxfrm() »). Современные системы GLIBC предоставляют отличную поддержку локалей, включая поддерживающие локали процедуры сопоставления регулярных выражений. Например, расширенное регулярное выражение POSIX [ [ :alpha: ] ] [ [ :alnum: ] ] + соответствует букве, за которой следуют одна или более букв или цифр (алфавитный символ, за которым следуют один или более алфавитно-цифровых символов). Определение того, какие символы соответствуют этим классам, зависит от локали. Например, это регулярное выражение соответствовало бы двум символам 'её', тогда как регулярное выражение [a-zA-Z] [a-A-ZzO-9]+ традиционного, ориентированного на ASCII Unix - скорее всего нет. Классы символов POSIX перечислены в табл. 13.5. Таблица 13.5. Классы символов регулярных выражений POSIX Класс Соответствует alnum: alpha: blank: cntrl: digit: graph: :lower: :print: :punct: : space: :upper: :xdigit Алфавитно-цифровые символы. Алфавитные символы. Символы пробела и табуляции. Управляющие символы. Цифровые символы. Символы, являющиеся одновременно печатными и видимыми. (Символ конца строки печатный, но не видимый, тогда как $ является и тем, и другим.) Строчные алфавитные символы. Печатные (не управляющие) символы. Знаки пунктуации (не буквы, цифры, управляющие или пробельные символы). Пробельные символы (такие, как сам пробел, символы табуляции, конца строки и т. д.). Заглавные алфавитные символы. Символы из набора abcdefABCDEFO 123456789.
13.5. Рекомендуемая литература 497 13.4.4. Заключение Возможно, вам никогда не придется иметь дело с различными наборами символов и их представлениями. С другой стороны, мир быстро становится «глобальным сообществом», и авторы программ не могут позволить себе быть ограниченными. Следовательно, стоит знать о проблемах интернационализации и наборов символов, а также способах их влияния на поведение вашей системы. По крайней мере, уже один из поставщиков дистрибутивов GNU/Linux устанавливает для систем в Соединенных Штатах локаль по умолчанию en_US .UTF- 8. 13.5. Рекомендуемая литература 1. С, A Reference Manual, 5th edition, by Samuel P. Harbison III and Guy L. Steele, Jr., Prentice-Hall, Upper Saddle River, New Jersey, USA, 2002. ISBN: 0-13-089592-X. Мы уже упоминали эту книгу раньше. Она дает точное и исчерпывающее описание развития и использования возможностей стандартной библиотеки С для работы с многобайтными и широкими символами. Это особенно ценно для современных систем, поддерживающих С99, поскольку библиотека была значительно усовершенствована для стандарта С 1999 г. 2. GNU gettext tools, by Ulrich Drepper, Jim Meyering, Framois Pinard, and Bruno Haible. Это руководство по GNU gettext. На системе GNU/Linux вы можете посмотреть локальную копию через 'info gettext'. Или загрузить и распечатать последнюю версию (по адресу ftp: //ftp.gnu.org/gnu/gettext/). 13.6. Резюме Интернационализация и локализация программ подпадают под общее название поддержки родного языка. Широко распространенными сокращениями являются И8п, ПОп и NLS. Центральным является понятие локали, которая позволяет настраивать набор символов, отображение даты, времени, денежных и числовых величин в соответствии с принятыми для данного языка и в данной стране нормами. • Использование локали устанавливается с помощью функции setlocale(). Различные категории локали предоставляют доступ к различным видам информации локали. Не использующие локаль программы действуют, как если бы они находились в локали «С», которая выдает типичные для систем Unix до NLS результаты: 7- разрядный ASCII, английские названия месяцев и дней и т. д. Локаль «POSIX» эквивалентна локали «С». Сравнение строк с учетом локали осуществляется функцией strcoll () или комбинацией strxf rm() и strcmp (). Возможности библиотеки предоставляют доступ к сведениям о локали (localeconv() и nl_langinfo ()), а также к специфического для локали форматирования (strfmon (), strf time () и printf ()). • Обратной стороной получения относящейся к локали информации является вывод сообщений на местном языке. Модель catgets () System V, хотя и стандартизована
498 Глава 13. Интернационализация и локализация POSIX, трудна для использования и поэтому не рекомендуется10. Вместо этого GNU gettext реализует и расширяет оригинальный замысел Solaris. • При использовании gettext () оригинальная строка сообщения на английском действует в качестве ключа в двоичном файле перевода, из которого получается перевод строки. Каждое приложение указывает уникальный текстовый домен таким образом, чтобы gettext () могла найти нужный файл с переводом (известный как «список сообщений»). Текстовый домен устанавливается с помощью функции textdomain (). При тестировании или иной надобности местоположение списка сообщений можно изменить с помощью функции bindt'extdomain (). • •• gettext () и ее варианты предоставляют доступ к переводам в различных текстовых доменах или различных категориях локалей. Вдобавок, функция ngettext () и ее варианты дают возможность делать правильные переводы множественных чисел, не перегружая разработчика. Указатель положения в спецификаторе формата printf () дает возможность перевода форматирующих строк, аргументы которых должны выводиться в другом порядке, чем они располагаются в строке. На практике GNU программы используют для пометки переводимых строк в исходных файлах заголовочный файл gettext. h и макросы _ () и N_ (). Такая практика обеспечивает удобочитаемость исходного кода и возможность его поддержки, предоставляя в то же время преимущества интернационализации и локализации. GNU gettext предоставляет многочисленные инструменты для создания и управления базами данных переводов (переносимых объектных файлов) и их двоичными эквивалентами (объектными файлами сообщений). Наконец, стоит быть в курсе проблем наборов символов и их представлений. Поставщики программного обеспечения не могут больше позволить себе предполагать, что их пользователи хотят работать лишь на одном языке. Упражнения 1. Поддерживает ли ваша система локали? Если да, какая локаль используется по умолчанию? 2. Просмотрите справочную страницу locale{\), если она у вас есть. Сколько имеется локалей, если вы посчитаете их с помощью'locale -a | wc -1'? 3. Поэкспериментируйте с chl3-strings .с, chl3-lconv.c, chl3-strfmon.c, chl3-quoteflag.с и chl3-times.c в различных локалях. Какая из найденных локалей самая «необычная» и почему? 4. Возьмите одну из своих программ. Интернационализируйте ее с использованием GNU gettext. Постарайтесь найти кого-нибудь, кто говорит на другом языке, чтобы перевести для вас сообщения. Откомпилируйте перевод и протестируйте его, использовав bindtextdotaain (). Какова была реакция вашего переводчика при виде использования перевода? GNU/Linux ее поддерживает, но лишь для совместимости. - Примеч. автора.
Глава 14 Расширенные интерфейсы В этой главе: • 14.1. Выделение выровненной памяти: posixjaemalign () и memal ign () 499 • 14.2. Блокировка файлов 500 • 14.3. Более точное время 512 • 14.4. Расширенный поиск с помощью двоичных деревьев 519 • 14.5. Резюме 529 • Упражнения. 530 15 данной главе описываются несколько расширенных API. Они сходны по своей природе с описанными ранее в книге или предоставляют дополнительные возможности. Некоторые из них трудно было свободно обсуждать, пока не были освещены предварительные темы. Порядок представления здесь соответствует порядку глав в первой половине книги. В другом отношении темы не связаны друг с другом. Мы освещаем следующие вопросы: динамическое выделение выровненной памяти; блокировку файлов; ряд функций, работающих со значениями долей секунды; и более развитый набор функций для сохранения и получения произвольных значений данных. Если не указано противное, все API в данной главе включены в стандарт POSIX. 14.1. Выделение выровненной памяти: posix_memalign () и memal ign () Для большинства задач отлично подходят стандартные процедуры выделения памяти - malloc (), realloc () и т. д. Но иногда может понадобиться память, которая выровнена тем или иным способом. Другими словами, адрес первого выделенного байта является кратным какого-нибудь числа. (Например, на некоторых системах копирование памяти осуществляется значительно быстрее, если используются буфера, выровненные по границе слова.) Такую службу предоставляют две функции: #include <stdlib.h> int posixjnemal ign (void **memptr, size_t alignment, size__t size); /* POSIX ADV */ void *memalign(size_t boundary, size_t size); /* Обычная */
500 Глава 14. Расширенные интерфейсы posix_memalign () является более новой функцией; она является частью другого необязательного расширения, «Консультативной информации» («Advisory Information»). Работа функции отличается от других функций выделения памяти Linux. При наличии проблемы она не возвращает -1. Вместо этого возвращаемое значение равно 0 при успехе или значению errno в случае неудачи. Аргументы следующие: void **memptr Указатель на переменную void *. Указываемая переменная будет содержать адрес выделенного блока памяти. Выделенная память освобождается с помощью free (). size__t alignment Требуемое выравнивание. Оно должно быть кратно sizeof (void *) и быть степенью двойки. size__t size Число выделяемых байтов. memalign() является нестандартной, но широко доступной функцией, которая работает сходным образом. Возвращаемое значение равно NULL в случае неудачи и запрошенному блоку памяти при успехе, причем boundary (степень двойки) обозначает выравнивание, a size - затребованный размер памяти. Традиционно выделенная memalign () память не могла быть освобождена с помощью free (), поскольку memalign () использовала для выделения памяти malloc () и возвращала указатель на выровненный подходящим образом байт где-то внутри блока. Версия GLIBC не имеет этой проблемы. Из этих двух функций следует использовать posix_memalign (), если она у вас есть. 14.2. Блокировка файлов Современные системы Unix, включая GNU/Linux, дают вам возможность заблокировать часть файла или весь файл для чтения или записи. Подобно многим частям Unix API, которые были разработаны после V7, имеется несколько несовместимых способов осуществить блокировку файлов. Данный раздел рассматривает эти возможности. 14.2.1. Концепции блокировки файлов Также, как замок на вашей двери предотвращает нежелательные проникновения в ваш дом, блокировка файла предотвращает доступ к данным в файле. Блокировка файлов была добавлена в Unix после разработки V7 (от которой происходят все современные системы Unix), и соответственно в течение некоторого времени в различных системах Unix были доступны и использовались несколько несовместимых механизмов блокировки файлов. Как в BSD Unix, так и в System V были собственные несочетающиеся вызовы для блокировки. В конечном счете POSIX формализовал способ осуществления блокировки
14.2. Блокировка файлов 501 файлов System V. К счастью, названия функций в System V и BSD были различны, так что GNU/Linux, в попытке угодить всем, поддерживает обе разновидности блокировок. Табл. 14.1 суммирует различные виды блокировок. Таблица 14.1. Функции блокировки файлов Источник BSD POSIX POSIX Функция flockO fcntK) lockf() Диапазон V Весь файл Чтение/ запись V < Вспомогательный Обязательный л/ V Имеются следующие аспекты блокировки файлов: Блокировка записей Блокировка записи является блокировкой части файла. Поскольку файлы Unix являются просто потоками байтов, было бы корректнее использовать термин блокировка диапазона (range lock), поскольку осуществляется блокировка диапазона байтов. Тем не менее, термин «блокировка записей» общеупотребительный. Блокировка всего файла Блокировка всего файла, как предполагает название, блокирует весь файл, даже если его размер меняется в блокированном состоянии. Интерфейс BSD предусматривает блокирование лишь всего файла. Для блокирования всего файла с использованием интерфейса POSIX указывают нулевую длину. Это интерпретируется особым образом как «весь файл». Блокировка чтения Блокировка чтения предотвращает запись в читаемую область. В файле может быть несколько заблокированных для чтения участков, даже в одной области файла, не мешающих друг другу, поскольку к данным осуществляется лишь доступ и они не изменяются. Блокировка записи Блокировка записи предоставляет исключительный доступ к записываемой области. Если эта область заблокирована также и для чтения, попытка получения блокировки записи либо блокируется, либо завершается неудачей в зависимости от запрошенного типа блокировки. После получения блокировки записи попытка получить блокировку чтения завершается неудачей. Вспомогательная блокировка Вспомогательная блокировка (advisory lock) тесно соответствует замку на двери. Говорят, «замки существуют для честных людей», что означает, что если кто-нибудь на самом деле захочет вломиться в ваш дом, он, возможно, найдет способ это сделать, несмотря на наличие замка в двери. То же и со вспомогательной блокировкой; она
502 Глава 14. Расширенные интерфейсы работает лишь тогда, когда тот, кто пытается получить доступ к заблокированному файлу, сначала пытается получить блокировку. Однако, программа может совершенно игнорировать вспомогательные блокировки и делать с файлом, что захочет (конечно, пока это разрешается правами допуска фдйла). Обязательная блокировка Обязательная блокировка является более строгой: когда установлена обязательная блокировка, ни один другой процесс не может получить доступ к заблокированному файлу. Любой процесс, который пытается игнорировать это, либо сам блокируется до снятия блокировки файла, либо его попытка завершится неудачей. (Под GNU/Linux по крайней мере это включает root!) Вспомогательная блокировка достаточна для взаимодействующих программ, разделяющих индивидуальный файл, когда не предполагается использование этого файла ни одним другим приложением. Обязательная блокировка подходит в ситуации, когда избежание конфликта в использовании файла является критическим, как в коммерческих системах баз данных. POSIX стандартизует лишь вспомогательную блокировку. Обязательная блокировка доступна на GNU/Linux, а также в ряде коммерческих систем Unix, но детали варьируют. Далее в данном разделе мы рассмотрим детали для GNU/Linux. 14.2.2. Блокировка POSIX: f cntl () и lockf () Системный вызов f cntl () (file control - управление файлом)^используется для блокировки файла. (Другое использование f cntl () было описано в разделе 9.4.3 «Управление атрибутами файла: f cntl () ».) Он объявлен следующим образом: #include <unistd.h> /* POSIX */ #include <fcntl.h> int fcntl(int fd, int cmd); Not relevant for file locking int fcntl(int fd, int cmd, long arg); Not relevant for file locking int fcntl(int fd, int cmd, struct flock *lock); Аргументы следующие: f d Дескриптор файла для открытого файла. cmd Одна или более именованных констант, определенных в <f cntl .h>. Ниже они описаны более подробно. lock Указатель на struct flock, описывающую нужный блок. 14.2.2.1. Описание блокировки Прежде чем рассмотреть осуществление блокировки, давайте исследуем описание блокировки в операционной системе. Это делается при помощи структуры struct flock, которая описывает диапазон блокируемых байтов и вид нужной блокировки. Стандарт POSIX утверждает, что struct lock содержит «по крайней мере» определен-
14.2. Блокировка файлов 503 ные члены. Это позволяет разработчикам предоставлять при желании дополнительные члены структуры. Из слегка отредактированной справочной страницы fcntlC): ;struct flock { short l_type; /* Тип блокировки: F_RDLCK, F_WRLCK, F_UNLCK */ short l_whence; /* Как интерпретируется l_start: SEEKJSET, SEEK_CUR, SEEK_END */ off_t l_start; /* Начальное блокируемое смещение *'/ off_t l_len; /* Число блокируемых байтов; 0 означает от начала до конца файла */ pid_t l_pid; /* PID блокирующего процесса (только F_GETLK) */ }; Поле l_start является смещением начального байта блокируемого участка. 1_1еп является длиной блокируемого участка, т. е. общим числом блокируемых байтов. 1_whence- указывает место в файле, относительно которого отсчитывается l_start; значения те же, что и для аргумента whence функции lseek () (см. раздел 4.5 «Произвольный доступ: перемещения внутри файла»), отсюда и название поля. Эта структура самодостаточна: смещение l__s tart и значение l_whence не связаны с текущим файловым указателем для чтения или записи. Пример кода мог бы выглядеть таким образом: struct employee { /* что угодно */ }; /* Описание сотрудника */ struct flock lock; /* Структура блока ¦*/ /* Заблокировать структуру для шестого сотрудника */ lock.l_whence = SEEK_SET; /* Абсолютное положение */ lock.l_start = 5 * sizeof(struct employee); /* Начало б-й структуры */ lock.l__len = sizeof(struct employee); /* Блокировать одну запись */ Используя SEEK_CUR или SEEK_END, вы можете заблокировать участки, начиная от текущего смещения в файле или относительно конца файла соответственно. Для этих двух случаев l_start может быть отрицательным, пока абсолютное начало не меньше нуля. Таким образом, чтобы заблокировать последнюю запись в файле: /* Заблокировать запись последнего сотрудника */ lock. l__whence = SEEK_END; /* Относительно EOF */ lock.l_start = -1 * sizeof (struct employee); /* Начало последней структуры */ lock.l__len = sizeof(struct employee); /* Заблокировать одну запись */ Установка 1_1еп в 0 является особым случаем. Он означает блокировку файла от начального положения, указанного с помощью l_start и l_whence, и до конца файла. Сюда входят также любые области за концом файла. (Другими словами, если заблокированный файл увеличивается в размере, область блокировки расширяется таким образом, чтобы продолжать охватывать весь файл.) Таким образом, блокирование-всего файла является вырожденным случаем блокирования одной записи: lock. l__whence •= SEEK_SET; /* Абсолютное положение */ lock.-l_s.tart = 0; /* Начало файла */ lock.l__len = 0; /* До конца файла */
504 Глава 14. Расширенные интерфейсы Справочная страница/лс//C) имеет примечание: POSIX 1003.1-2001 допускает отрицательные значения 1_1еп. (И если это так, описываемый блоком интервал охватывает байты с l_start + l__len вплоть до l_start - 1 включительно.) Однако, в этой ситуации системный вызов Linux для современных ядер возвращает EINVAL. (Мы заметили, что справочная страница относится к версиям ядер 2.4.x; стоит проверить текущую справочную страницу, если ваша система новее.) Теперь, когда мы знаем, как описать где блокируется файл, мы можем описать тип блокировки с помощью l_type. Возможные значения следующие: F_RDLCK Блокировка чтения. Для применения блокировки чтения файл должен быть открыт для чтения. F_WRLCK Блокировка записи. Для применения блокировки записи файл должен быть бткрыт для записи. F_UNLCK Освобождение предыдущей блокировки. Таким образом, полная спецификация блокировки включает установку в структуре struct flock значений четырех полей: трех для указания блокируемой области и четвертого для описания нужного типа блока. Значение F_UNLCK для l_type снимает блокировку. В общем, это простейший способ снять те самые блоки, которые были установлены ранее, но можно «расщепить» блок, освободив диапазон байтов в середине ранее установленного более крупного блока. Например: struct employee { /* что угодно */ }; /* Описание сотрудника */ struct flock lock; /* Структура блока */ /* Заблокировать сотрудников 6-8.*/ lock.l_whence = SEEK__SET; /* Абсолютное положение */ lock.l_start = 5 * sizeof(struct employee); /* Начало 6-й структуры */ lock.l__len = sizeof(struct employee) * 3; /* Заблокировать 3 записи */ /* ...установка блокировки (см. следующий раздел)... */ /* Освобождение записи 7: предыдущий блок расщепляется на два: */ lock.l_whence = SEEK__SET; /* Абсолютное положение */ lock.l_start = б * sizeof(struct, employee); /*.Начало 7-й структуры */ lock.l_len = sizeof (struct employee) * 1; /* Разблокирование 1-й записи */ /* ...снятие блокировки (см. следующий раздел)... */ 14.2.2.2. Установка и снятие блокировок После заполнения структуры struct flock следующим шагом является запрос блокировки. Этот шаг осуществляется с помощью соответствующего значения аргумента cmd функции f cntl ():
14.2. Блокировка файлов 505 F_GETLK Узнать, можно ли установить блокировку. F_SETLK Установить или снять блокировку. F_SETLKW Установить блокировку, подождав, пока это будет возможным. Команда F_GETLK является командой «Мама, можно мне?» Она осведомляется, доступна ли описанная struct flock блокировка. Если она доступна, блокировка не устанавливается; вместо этого операционная система изменяет поле l_type на F_UNLCK. Другие поля остаются без изменений. Если блокировка недоступна, операционная система заполняет различные поля сведениями, описывающими уже установленные блокировки, которые препятствуют установке новой. В этом случае l_pid содержит PID процесса, владеющего соответствующей блокировкой1. Если блокировка уже установлена, нет другого выбора, кроме ожидания в течение некоторого времени и новой попытки установки блокировки или вывода сообщения об ошибке и отказа от дальнейших попыток. Команда F_SETLK пытается установить указанную блокировку. Если f cntl () возвращает 0, блокировка была успешно установлена. Если она возвращает -1, блокировку установил другой процесс. В этом случае в errno устанавливается либо EAGAIN (попытайтесь снова позже) или EACCESS (нет доступа). Возможны два значения, чтобы удовлетворить старым системам. - Команда F_SETLKW также пытается установить указанную блокировку. Она отличается от F_SETLK тем, что будет ждать, пока установка блокировки не окажется возможной. Выбрав соответствующее значение для аргумента ст&, передайте его в качестве второго аргумента fcntlO вместе с указателем на заполненную структуру struct flock в качестве третьего аргумента: struct flock lock; int fd; /* ...открыть файл, заполнить struct flock... */ if (fcntl(fd, F_SETLK, & lock) < 0) { /* Установить не удалось, попытаться восстановиться */ } Функция lockf (J предоставляет альтернативный способ установки блокировки в текущем положении файла'. #include <sys/file.h> /* XSI */ int lockf(int fd, int cmd, off_t len); Справочная страница GNU/Linuxyc/2//C) указывает, что этих сведений может быть недостаточно; процесс может находиться на другой* машине! При блокировках по сети есть и другие проблемы; в общем, использование блокировки в файловых системах, смонтированных для удаленных компьютеров, не является удачной мыслью. - Примеч. автора. В системе GNU/Linux lockf () реализована в виде «оболочки» вокруг f cntl (). - Примеч. автора.
506 Глава 14. Расширенные интерфейсы Дескриптор файла f d должен быть открыт для записи, len указывает число блокируемых байтов: от текущего положения (назовем его pos) до pos + len байтов, если len положительно, или от pos - len до pos - 1, если len отрицательно. Команды следующие: F__LOCK Устанавливает исключительную блокировку диапазона. Вызов блокируется до тех пор, пока блокировка диапазона не станет возможной. F_TLOCK Пытается установить блокировку. Это похоже на F_LOCK, но если блокировка недоступна, F_TLOCK возвращает ошибку. F_ULOCK Разблокирует указанный раздел. Это может вызвать расщепление блокировки, как описано выше. F__TEST Проверяет, Доступна ли блокировка. Если доступна, возвращает 0 и устанавливает блокировку. В противном случае возвращает-1 и устанавливает в errno EACCESS. Возвращаемое значение равно 0 в случае успеха и -1 при ошибке, с соответствующим значением в errno. Возможные значения ошибок включают: EAGAIN Файл заблокирован, для F_TLOCK или F_TEST. EDEADLK Для F_TLOCK эта операция создала бы тупика. ENOLCK Операционная система не смогла выделить блок. а. Тупик (deadlock) является ситуацией, при которой оба процесса блокируются, причем каждый .из них ждет, пока другой освободит определенный ресурс. - Пргшеч. автора. Полезна комбинация F_TLOCK и EDEADLK: если вы знаете, что тупик не может возникнуть никогда, используйте F_LOCK. В противном случае, стоит обезопасить себя и использовать F__TLOCK. Если блокировка доступна, она осуществляется, но если нет, у вас появляется возможность восстановления вместо блокирования в ожидании, возможно, навечно. Завершив работу с заблокированным участком, его следует освободить. Для f cntl () возьмите первоначальную struct lock, использованную для блокирования, и измените поле l_type на F_UNLCK. Затем используйте F_SETLK в качестве аргумента cmd: lock.l_whence = ... ; /* Как раньше */ lock.l_start = ... ; /* Как раньше */ lock.l_len = ... ; /* Как раньше */ lock.l_type = F_UNLCK; /* Разблокировать */ * if (fcntl(fd, F_SETLK, & lock) < 0) { /* обработать ошибку .*/ } /* Блокировка была снята */ Код, использующий lockf (), несколько проще. Для краткости мы опустили проверку ошибок: off_t curpos, len; curpos = lseek(fd/ (off_t) 0, SEEK_CUR); /* Получить текущее положение */
14.2. Блокировка файлов 507 len = ... ; /* Установить соответствующее число блокируемых байтов */ lockf(fd, F_LOCK, len); /* Осуществить блокировку */ /* ...здесь использование заблокированного участка... */ lseek(fd, curpos, SEEK_SET) ; /* Вернуться к началу блокировки */ lockf(fd, F__ULOCK, len); /* Разблокировать файл */ Если вы не освободите блокировку явным образом, операционная система сделает это за вас в двух случаях. Первый случай, когда процесс завершается (либо при возвращении из main (), либо с использованием функции exit (), которую мы рассматривали в разделе 9.1.5.1 «Определение статуса завершения процесса»). Другим случаем является вызов close () с дескриптором файла: больше об этом в следующем разделе. 14.2.2.3. Предостережения по поводу блокировок Имеется несколько предостережений, о которых нужно знать при блокировках файлов: Как описано ранее, вспомогательная блокировка является именно этим. Не взаимодействующий процесс может делать все, что хочет, за спиной (так сказать) процесса, осуществляющего блокировку. Эти вызовы не следует использовать в сочетании с библиотекой <stdio.h>. Эта библиотека осуществляет свое собственное буферирование. Хотя вы можете получить с помощью fileno() дескриптор нижележащего файла, действительное положение в файле может быть не там, где вы думаете. В общем, стандартная библиотека ввода/вывода не понимает блокировок файлов. Держите в уме, что блокировки после fork не наследуются порожденными процессами, но они остаются на своем месте после exec. • Вызов close () с любым открытым для файла дескриптором удаляет все блокировки файла процессом, даже если другие дескрипторы для файла остаются открытыми. То, что close () работает таким образом, является неудачным, но поскольку так была реализована первоначальная блокировка в f cntl (), POSIX ее стандартизует. Стандартизация такого поведения позволяет избежать порчи существующего кода для Unix. 14.2.3. Блокирование BSD: flock () 4.2 BSD представило свой собственный механизм блокировки, flock О3. Функция объявлена следующим образом: #include <sys/file.h> /* Обычный */ int flock(int fd, int operation); Дескриптор f d представляет открытый файл. Имеются следующие операции: Удачно, что название flock () отличается от lockf (), поскольку их семантика различна. Это также страшно сбивает с толку. Держите свое руководство под рукой. - Примеч. автора.
508 Глава 14. Расширенные интерфейсы LOCK_SH Создает совместную блокировку. Может быть несколько совместных блокировок. LOCK_EX Создает исключительную блокировку. Может быть лишь одна такая блокировка. LOCK_UN Удаляет предыдущую блокировку. LOCKJNTB При использовании побитового ИЛИ с L0CK_JSH или LOCK__EX позволяет избежать блокирования функции, если блокировка файла невозможна. По умолчанию запросы блокировки файла будут блокировать функцию (не давать ей вернуться), если существует конкурирующая блокировка. Запрашивающая функция возвращается, когда конкурирующая блокировка файла снимается и осуществляется запрошенная функцией блокировка файла. (Это предполагает, что по умолчанию имеется возможность возникновения тупика.) Чтобы попытаться заблокировать файл без блокирования функции, добавьте посредством побитового ИЛИ значение LOCK_NB к имеющемуся значению operation. Отличительными моментами flock () являются следующие: блокировка с помощью f lock() является вспомогательной; программа, не использующая блокировку, может прийти и испортить без всяких сообщений об ошибках файл, заблокированный с помощью flock (). Блокируется весь файл. Нет механизма для блокировки только части файла. То, как был открыт файл, не влияет на тип блокировки, который может быть использован. (Сравните это с f cntl (), при использовании которой файл должен быть открыт для чтения для получения блокировки чтения, или для записи для блокировки записи.) • Несколько открытых для одного и того же файла дескрипторов используют совместную блокировку. Для удаления блокировки может использоваться любой из них. В отличие от f cntl (), когда нет явного разблокирования, блокировка не удаляется до тех пор, пока не будут закрыты все открытые дескрипторы файла. Процесс может иметь лишь одну блокировку файла с помощью flock (); последовательный вызов flock () с двумя различными типами блокировок изменяет тип блокировки на новый. • На системах GNU/Linux блокировки flock () совершенно независимы от блокировок fcntl (). Многие коммерческие системы Unix реализуют flock () в виде «оболочки» поверх f cntl (), но их семантика различается. Мы не рекомендуем использовать flock () в новых программах, поскольку ее семантика не такая гибкая и поскольку она не стандартизована POSIX. Поддержка ее в GNU/ Linux осуществляется главным образом для обратной совместимости с программным обеспечением, написанным для старых систем BSD Unix. I ЗАМЕЧАНИЕ. Справочная страница GNU/Linux flockB) предупреждает, что блокировки flock () не работают для смонтированных удаленных файлов. Блокировки I fcntl () работают, при условии, что у вас достаточно новая версия Linux и сервер I NFS поддероюивает блокировки файлов.
14.2. Блокировка файлов 509 14.2.4. Обязательная блокировка Большинство коммерческих систем Unix поддерживают в дополнение к вспомогательной обязательную блокировку файлов. Обязательная блокировка работает лишь с f cntl (). Обязательная блокировка файла контролируется установками прав доступа файла, в частности, путем добавления к файлу бита setgid с помощью команды chmod: $ echo hello, world > my file /* Создать файл */ $ Is -1 myfile /* Отобразить права доступа */ -rw-r—г-- 1 arnold devel 13 Apr 3 17:11 myfile $ chmod g+s myfile /* Добавить бит setgid */ $ Is -1 myfile /* Показать новые права доступа */ -rw-r-Sr-- 1 arnold devel 13 Apr 3 17:11 myfile Бит права на исполнение группой должен быть оставлен сброшенным. S показывает, что бит setgid установлен, но что бит права на исполнение - нет; если бы были установлены оба бита, была бы использована строчная буква s. Комбинация установленного бита setgid и сброшенного бита права на исполнение группой обычно бессмысленно. По этой причине, она была выбрана разработчиками System V для обозначения «использования обязательного блокирования». И в самом деле, добавления этого бита достаточно, чтобы заставить коммерческую систему Unix, такую как Solaris, использовать блокировку файлов. На системах GNU/Linux несколько другая история. Для обязательных блокировок файл должен иметь установленный бит setgid, но этого одного недостаточно. Файловая система, содержащая файл, также должна быть смонтирована с опцией mand в команде mount. Мы уже рассмотрели файловые системы, разделы диска, монтирование и команду mount, главным образом, в разделе 8.1 «Монтирование и демонтирование файловых систем». Мы можем продемонстрировать обязательную блокировку с помощью небольшой программы и файловой системой для тестирования на гибком диске. Для начала, вот программа: I /* chl4-lockall .с Демонстрация обязательной блокировки. ¦*/ 2 3 #include <stdio.h> /* для fprintf(), stderr, BUFSIZ */ 4 #include <errno.h> /* объявление errno */ 5 #include <fcntl.h> /* для флагов open() */ 6 #include <string.h> /* объявление strerrorO */ 7 #include <unistd.h> /* для ssize__t */ 8 #include <sys/types.h> 9 #include <sys/stat.h> /* для mode_t */ 10 II int 12 main(int'argc, • char **argv) 13 { 14 int fd; 15 int i/ j;
510 Глава 14. Расширенные интерфейсы 16 mode_t rw_mode; 17 static char message[] = "hello, world\n"; 18 struct flock lock; 19 20 if (argc != 2) { 21 fprintf(stderr, "usage: %s file\n", argv[0]); 22 exitA); 23 } 24 2 5 rw_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; /* 0644 */ 26 fd = open(argv[1], 0_RDWR|0_TRUNC|0_CREAT|0__EXCL, rw_mode); 27 if (fd < 0) { 28 fprintf(stderr, "%s: %s: cannot open for read/write: %s\n", 29 - argv[0], argv[l], strerror(errno)); 30 (void) close(fd); 31 return 1; 32 } 33 34 if (write(fd, message, strlen(message)) != strlen(message)) { 35 fprintf(stderr, "%s: %s: cannot write: %s\n", 3 6 argv[0], argv[l], strerror(errno)); 37 (void) close(fd); 3 8 return 1; 39 } 40 41 rw_mode |= S_ISGID; /* добавить бит обязательной блокировки */ 42. 43 if (fchmod(fd, rw_mode) < 0) { 44 fprintf(stderr, "%s: %s: cannot change mode to %o: %s\n"/ 4-5 argv[0] , argv[l] , rw_mode, strerror (errno) ) ; 46 (void) close(fd),• 47 return 1; 48 } 49 50 /* заблокировать файл ¦*/ 51 memset(& lock, '\0', sizeof(lock)); 52 lock.l_whence = SEEK_SET; .53 lock.l_start = 0; 54 lock.l__len = 0; /* блокировка всего файла.*/ 55 lock.l_type = F__WRLCK; /* блокировка записи */ 56 57 if (fcntMfd, F^SETLK, & lock) < 0) { 58 fprintf(stderr, "%s: %s: cannot lock the file: %s\n", 59 argv[0], argv[l], strerror(errno));
14.2. Блокировка файлов 511 60 (void) close(fd); 61 return 1; 62 } 63 64 pause(); 65 66 (void) close(fd); 67 68 return 0; 69 } Программа устанавливает права доступа и создает файл, указанный в командной строке (строки 25 и 26). Затем она записывает в файл некоторые данные (строка 34). Строка 41 добавляет к правам доступа бит setgid, а строка 43 изменяет их. (Системный вызов f chmod () обсуждался в разделе 5.5.2 «Изменение прав доступа: chmod () и f chmod () ».) Строки 51-55 устанавливают struct flock для блокировки всего файла, а затем блокировка осуществляется реально в строке 57. Выполнив блокировку, программа засыпает, используя системный вызов pause () (см. раздел 10.7 «Сигналы для межпроцессного взаимодействия»). После этого программа закрывает дескриптор файла и завершается. Вот расшифровка с комментариями, демонстрирующая использование обязательной блокировки файлов: $ fdformat /dev/fdO /* Форматировать гибкий диск */ Double-sided, 80 tracks, 18 sec/track. Total capacity 1440 kB. Formatting ... done Verifying ... done $ /sbin/znke2?s /dev/fdO /* Создать файловую систему Linux */ /* ...множество вывода опущено... */ $ su /* Стать root, чтобы использовать mount */ Password: /* Пароль не отображается */ # mount -t ext2 -о znand /dev/fdO /mnt/floppy /* Смонтировать гибкий диск, с возможностью блокировок */ # suspend /* Приостановить оболочку root •*/ i [1]-*- Stopped su $ chl4-lockall /mnt/floppy/x & /* Фоновая программа */ [2] 23311 /* содержит блокировку */ $ Is -l /mnt/floppy/x /* Посмотреть файл */ -rw-r-Sr-- l arnold devel 13 Apr 6 14:23 /mnt/floppy/x $ echo something > /mnt/floppy/x У* Попытаться изменить файл */ bash2: /mnt/floppy/x: Resource temporarily unavailable /* Возвращается ошибка */ $ kill %2 /* Завершить программу с блокировкой */ $ /* Нажать ENTER */ [2]- Terminated chl4-lockall /mnt/floppy/x /* Программа завершена */ $ echo something > /mnt/floppy/x' /* Новая попытка изменения работает */ $ fg /* Вернуться в оболочку root */
512 Глава 14. Расширенные интерфейсы SU # uxnount /ront/ floppy /* Демонтировать гибкий диск */ # exit /* Работа с оболочкой root закончена •*/ $ ¦ ¦ Пока выполняется chl4-lockall, она владеет блокировкой. Поскольку это обязательная блокировка, перенаправления ввода/вывода оболочки завершаются неудачей. После завершения chl4-lockall блокировки освобождаются, и перенаправление ввода/вывода достигает цели. Как упоминалось ранее, под GNU/Linux даже root не может аннулировать обязательную блокировку файла. Немного отклоняясь в сторону, гибкие диски представляют отличный испытательный стенд для изучения того, как использовать инструменты, работающие с файловыми системами. Если вы сделаете что-то, что разрушит данные на гибком диске, это вряд ли будет катастрофическим, тогда как экспериментирование с действующими разделами на обычных жестких дисках значительно более рискованно. 14.3. Более точное время Системный вызов time () и тип time__t представляют время в секундах в формате отсчета с начала Эпохи. Разрешения в одну секунду на самом деле недостаточно; сегодняшние машины быстры, и часто бывает полезно различать временные интервалы в долях секунды. Начиная с 4.2 BSD, Berkley Unix представил ряд системных вызовов, которые сделали возможным получение и использование времени в долях секунд. Эти вызовы доступны на всех современных системах Unix, включая GNU/Linux. 14.3.1. Время в микросекундах: gettimeof day () Первой задачей является получение времени дня: #include <sys/time.h> int gettimeof day (struct timeval *tv, void *tz); /* определение POSIX, а не GLIBC */ gettimeofday() позволяет получить время дня4. В случае успеха возвращается О, при ошибке -1. Аргументы следующие: struct timeval *tv Этот аргумент является указателем на struct timeval, которая вскоре будет описана и в которую система помещает текущее время. void *tz Это аргумент больше не используется; он имеет тип void*, поэтому он всегда должен равняться NULL. (Справочная страница описывает, для чего он использовался, а затем утверждает, что он устарел. Прочтите, если интересуетесь подробностями.) В справочной странице gettimeofdayB) документирована соответствующая функция settimeofday () для использования суперпользователем (root) для установки времени дня всей системы. - Примеч. автора.
14.3. Более точное время 513 Время представлено структурой struct timeval: struct timeval { long tv__sec; /* секунды */¦ long tv_usec; /* микросекунды *¦/ } ; Значение tv_sec представляет секунды с начала Эпохи; tv_usec является числом микросекунд в секунде. Справочная страница GNU/Linux gettimeofdayB) документирует также следующие макросы: #define timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec) #define timercmp(tvp, uvp, cmp) \ ((tvp)->tv_sec cmp (uvp)->tv_sec || \ (tvp)->tv_sec == (uvp)->tv_sec && \ (tvp)->tv_usec cmp (uvp)->tv_usec) #define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv__usec = 0) Эти макросы работают со значениями struct timeval *; то есть указателями на структуры, и их использование должно быть очевидным из их названий и кода. Особенно интересен макрос timercmp(): третьим аргументом является оператор сравнения для указания вида сравнения. Например, рассмотрим определение того, является ли одна struct timeval меньше другой: struct timeval tl, t2; . if (timercmp(& tl, & t2, <)) /* tl меньше, чем t2 */ Макрос развертывается в ((& tl)->tv_sec < (& t2)->tv_sec || \ (& tl)->tv_sec == (& t2)->tv_sec && \ (& tl)->tv_usec < (Sc t2)->tv_usec) Это значит: «если tl.tv_sec меньше, чем t2.tv_sec, ИЛИ если они равны и tl. tv_usec меньше, чем t2 . tv_usec, тогда ...». 14.3.2. Файловое время в микросекундах: utimes () В разделе 5.5.3 «Изменение временных отметок: utime ()» был описан системный вызов utime () для установки времени последнего обращения и изменения данного файла. Некоторые файловые системы хранят эти временные отметки с разрешением в микросекунды (или еще точнее). Такие системы предусматривают системный вызов utimes () (обратите внимание на завершающую s в названии) для установки времени обращения к файлу и его изменения с точностью до микросекунд: #include <sys/time.h> /* XSI */ int utimes(char *filename, struct timeval tvp[2]); 17 -159
514 Глава 14. Расширенные интерфейсы Аргумент tvp должен указывать на массив из двух структур struct timeval; значения используются для времени доступа и изменения соответственно. Если tvp равен NULL, система использует текущее время дня. POSIX обозначает ее как «традиционную» функцию, что означает, что она стандартизуется лишь для поддержки старого кода и не должна использоваться для новых приложен ний. Главная причина, пожалуй, в том, что нет определенного интерфейса для получения времени доступа и изменения файла в микросекундах; struct stat содержит лишь значения time_t, а не значения struct timeval. Однако, как упоминалось в разделе 5.4.3 «Только Linux: указание файлового времени повышенной точности», Linux 2.6 (и более поздние версии) действительно предоставляет доступ к временным отметкам с разрешением в наносекунды при помощи функции stat (). Некоторые другие системы (такие, как Solaris) также это делают5. Таким образом, utimes () полезнее, чем кажется на первый взгляд, и несмотря на ее «традиционный» статус, нет причин не использовать ее в своих программах. 14.3.3. Интервальные таймеры: setitimer () и getitimer () Функция alarm() (см. раздел 10.8.1 «Сигнальные часы: sleepO, alarm() и SIGALRM») организует отправку сигнала SIGALRM после истечения данного числа секунд. Ее предельным разрешением является одна секунда. Здесь также BSD 4.2 ввело функцию и три различных таймера, которые используют время в долях секунды. Интервальный таймер подобен многократно использующимся сигнальным часам. Вы устанавливаете начальное время, когда он должен «сработать», а также как часто это должно впоследствии повторяться. Оба этих значения используют объекты struct timeval; т. е. они (потенциально) имеют разрешение в микросекундах. Таймер «срабатывает», доставляя сигнал; таким образом, нужно установить для таймера обработчик сигнала, желательно до установки самого таймера. Существуют три различных таймера, описанных в табл. 14.2. Таблица 14.2. Интервальные таймеры Таймер Сигнал Функция ITIMER_REAL SIGALRM Работает в реальном режиме. ITIMER_VIRTUAL SIGVTALRM Работает, когда процесс выполняется в режиме пользователя. ITIMER__PROF SIGPROF Работает, когда процесс выполняется в режиме пользователя или ядра. К сожалению, по-видимому, в настоящее время нет стандарта для названий членов struct stat, что делает такую операцию непереносимой. - Примеч. автора.
14.3. Более точное время 515 Использование первого таймера, ITIMER_REAL, просто. Таймер работает в реальном времени, посылая SIGALRM по истечении заданного количества времени. (Поскольку посылается SIGALRM, нельзя смешивать вызовы setitimerO с вызовами alarmO, а смешивание их с вызовом sleep () также опасно; см. раздел 10.8.1 «Сигнальные часы: sleep(),'alarm() и SIGALRM».) Второй таймер, ITIMER_VIRTUAL, также довольно прост. Он действует, когда процесс исполняется, но лишь при выполнении кода пользователя (приложения). Если процесс заблокирован во время ввода/вывода, например, на диск, или, еще важнее, на терминал, таймер приостанавливается. Третий таймер, ITIMER_PROF, более специализированный. Он действует все время, пока выполняется процесс, даже если операционная система делает что-нибудь для процесса (вроде ввода/вывода). В соответствии со стандартом POSIX, он «предназначен для использования интерпретаторами при статистическом профилировании выполнения интерпретируемых программ». Установив как для ITIMER_VTRTUAL, так и для ITIMER_PROF идентичные интервалы и сравнивая разницу времени срабатывания двух таймеров, интерпретатор может узнать, сколько времени проводится в системных вызовах для выполняющейся интерпретируемой программы6. (Как сказано, это довольно специализировано.) Двумя системными вызовами являются: #include <sys/time.h> /* XSI */ int getitimer(int which, struct itimerval *value); int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue); Аргумент which является одной из перечисленных ранее именованных констант, указывающих таймер, getitimer () заполняет struct itimerval, на которую указывает value, текущими установками данного таймера, setitimer () устанавливает для данного таймера значение в value. Если имеется ovalue, функция заполняет ее текущим значением таймера. Используйте для ovalue NULL, если не хотите беспокоиться о текущем значении. Обе функции возвращают в случае успеха 0 и -1 при ошибке, struct itimerval состоит из двух членов struct timeval: struct itimerval { struct timeval it_interval; /* следующее значение */ struct timeval it_value; /*. текущее значение */ }; Прикладным программам не следует ожидать, что таймеры будут с точностью до микросекунд. Справочная страница getitimeiil) дает следующее объяснение: Таймеры никогда не срабатывают раньше заданного времени, вместо этого срабатывая спустя небольшой постоянный интервал времени, зависящий от разрешения системного таймера (в настоящее время 10 мс). После срабатывания будет сгенерирован Корректное выполнение профилировки нетривиальная задача; если вы думаете о написании интерпретатора, стоит сначала провести свои исследования. - Примеч. автора 1«Т*
516 Глава 14. Расширенные интерфейсы сигнал, а таймер будет сброшен. Если таймер срабатывает, когда процесс выполняется (для таймера ITIMER_VIRT это всегда верно), сигнал будет доставлен немедленно после создания. В противном случае, доставка будет сдвинута на небольшой промежуток времени, зависящий от загрузки системы. Из этих трех таймеров ITIMER__REAL кажется наиболее полезным. Следующая программа, chl4-timers . с, показывает, как читать данные с терминала, но с тайм-аутом, чтобы программа не зависала на бесконечное время, ожидая ввода: I /* ch.14-timers .с -* демонстрация интервальных таймеров */ 2 3 #include <stdio.h> 4 #include <assert.h> 5 #include <signal.h> 6 #include <sys/tinie.h> 7 8 /¦* handler обрабатывает SIGALRM */ 9 10 void handler(int signo) II { 12 static const char msg[] = "\n*** Timer expired, you lose ***\n"; 13 14 assert(signo == SIGALRM); 15 16 writeB, msg, sizeof(msg) - 1); 17 exit(l); 18 } 19 20 /* main установить таймер, прочесть данные с тайм-аутом */ 21 22 int main(void) 23 { 24 struct itimerval tval; 25 char string[BUFSIZ]; 26 27 timerclear(& tval. it_interval); /* нулевой интервал означает не сбрасьвать таймер */ 28 timerclear(& tval.it_value); 29 30 tval. it__value. tv_sec = 10; /* тайм-аут 10 секунд */ 31 32 (void) signal(SIGALRM, handler); 33 34 printf("You have ten seconds to enter\nyour name, rank, and serial number: "); 35
14.3. Более точное время 517 36 (void) setitimer(ITIMER_REAL, & tval, NULL); 37 if (fgets(string, sizeof string, stdin) != NULL) { ;V 38 (void) setitimer (ITIMER_REAL, NULL, NULL).; /* выключить .таймер */ 39 /* обработать оставшиеся данные, вывод диагностики для иллюстрации */ 40 printf("I'm glad you are being cooperative.\n"); . i . ! • 41 } else 42 printf("\nEOF, eh? We won'.t give up so easily! \n"); 43 44 exit@); 45'} Строки 10-18 представляют обработчик сигнала для SIGALRM; вызов assert () гарантирует, что обработчик сигнала был установлен соответствующим образом. Тело обработчика выводит сообщение и выходит, но оно может делать что-нибудь более подходящее для крупномасштабной программы. В функции main () строки 27-28 очищают два члена struct timeval структуры struct itimerval, tval. Затем строка 30 устанавливает тайм-аут в 10 секунд. Установка tval. it_interval в 0 означает, что нет повторяющегося сигнала; он срабатывает лишь однажды. Строка 32 устанавливает обработчик сигнала, а строка 34 выводит приглашение. Строка 36 устанавливает таймер, а строки 37-42 выводят соответствующие сообщения, основываясь на действиях пользователя. Реальная программа выполняла бы в этот момент свою задачу. Важно здесь обратить внимание на строку 38, которая отменяет таймер, поскольку были введены действительные данные. ^ЗАМЕЧАНИЕ. Между строками 37 и 38 имеется намеренное состояние гонки. Все Хдело в том, что если пользователь не вводит строку в течение отведенного {таймером времени, будет доставлен сигнал, и обработчик сигнала выведет сооб- \щение «you lose». Вот три успешных запуска программы: $ chl4-timers /* Первый запуск, ничего не вводится */ You have ten seconds to enter your name, rank, and serial number: *** Timer expired, you lose *** $ chl4-timers /* Второй запуск, ввод данных */ You have ten seconds to enter your name, rank, and* serial number: James Kirk, Starfleet Captain, 1234 I'm glad you are being cooperative. $ chl4-timers /* Третий запуск, ввод EOF (AD) */ You have ten seconds to enter your name, rank, and serial number: AD EOF, eh? We won't give up so easily!
518 Глава 14. Расширенные интерфейсы POSIX оставляет неопределенным, как интервальные таймеры взаимодействуют с функцией sleepO, если вообще взаимодействуют. GLIBC не использует для реализации sleep () функцию alarm (¦), поэтому на системах GNU/Linux sleep () не взаимодействует с интервальным таймером. Однако, для переносимых программ, вы не можете делать такое предположение. 14.3.4. Более точные паузы: nanosleep () Функция sleep () (см. раздел 10.8.1 «Сигнальные часы: sleep(), alarm() и SIGALRM») дает программе возможность приостановиться на указанное число секунд. Но, как мы видели, она принимает лишь целое число секунд, что делает невозможным задержки на короткие периоды, она потенциально может также взаимодействовать с обработчиками SIGALRM. Функция nanosleep () компенсирует эти недостатки: #include <time.h> /* POSIX TMP */ int nanosleep(const struct timespec *req, struct timespec *rem); Эта функция является частью необязательного расширения POSIX «Таймеры» (TMR). Два аргумента являются запрошенным временем задержки и оставшимся числом времени в случае раннего возвращения (если rem не равен NULL). Оба являются значениями struct timespec: v struct timespec { time_t tv_sec; /* секунды */ long tv_nsec; /* наносекунды */ }; Значение tv_nsec должно быть в диапазоне от 0 до 999 999 999. Как и в случае со sleep (), время задержки может быть больше запрошенного в зависимости оттого, когда и как ядро распределяет время для исполнения процессов. В отличие от sleep (), nanosleep () не взаимодействует ни с какими сигналами, делая ее более безопасной и более простой для использования. Возвращаемое значение равно 0, если выполнение процесса было задержано в течение всего указанного времени. В противном случае оно равно -1, с errno, указывающим ошибку. В частности, если errno равен EINTR, nanosleep () была прервана сигналом. В этом случае, если rem не равен NULL, struct timespec, на которую она указывает, содержит оставшееся время задержки. Это облегчает повторный вызов nanosleep () для продолжения задержки. Хотя это выглядит немного странным, вполне допустимо использовать одну и ту же структуру для обоих параметров: struct timespec sleeptime = /* что угодно */ ; int ret; ret = nanosleep(& sleeptime, & sleeptime);
14.4. Расширенный поиск с помощью двоичных деревьев 519 struct timeval и struct timespec сходны друг с другом, отличаясь лишь компонентом долей секунд. Заголовочный файл GLIBC <sys/time.h> определяет для их взаимного преобразования друг в друга два полезных макроса: •¦# include <sys/time.h> /* GLIBC*/-. void TIMEVAL_TO_TIMESPEC(struct timeval *tv, struct timespec *ts) ; void TIMEPSEC_TO_TIMEVAL(struct timespec *ts, struct timeval *tv) ; Вот они: # define TIMEVAL_TO_TIMESPEC(tv, ts) { \ (ts)->tv_sec = (tv)->tv_sec; \ (ts)->tv_nsec = (tv)->tv_usec * 1000; \ } # define TIMESPEC_TO_TIMEVAL(tv, ts) { \ (tv)->tv_sec = (ts)->tv_sec; \ (tv)->tv_usec = (ts)->tv_nsec / 1000; \ } #endif ХЗАМЕЧАНИЕ. To, что некоторые системные вызовы используют микросекунды, I а другие - наносекунды, в самом деле сбивает с толку. Причина этого историче- I екая: микросекундные вызовы были разработаны на системах, аппаратные часы I которых не имели более высокого разрешения, тогда как наносекундные вызовы Хбыли разработаны более недавно для систем со значительно более точными I часами. С'est la vie. Почти все, что вы можете сделать, это дерэ/сать под руками I ваше руководство. 14.4. Расширенный поиск с помощью двоичных деревьев В разделе 6.2 «Функции сортировки и поиска» мы представили функции для поиска и сортировки массивов. В данном разделе мы рассмотрим более продвинутые возможности. 14.4.1. Введение в двоичные деревья Массивы являются почти простейшим видом структурированных данных. Их просто понимать и использовать. Хотя у н"их есть недостаток, заключающийся в том, что их размер фиксируется во время компиляции. Таким образом, если у вас больше данных, чем помещается в массив, вам не повезло, Если у вас значительно меньше данных, чем размер массива, память расходуется зря. (Хотя на современных системах много памяти, подумайте об ограничениях программистов, пишущих программы для внедренных систем, таких, как микроволновые печи и мобильные телефоны. С другого конца спектра, подумайте о проблемах программистов, имеющих дело с огромными объемами ввода, таких, как прогнозирование погоды.
520 Глава 14. Расширенные интерфейсы В области компьютерных наук были придуманы многочисленные динамические структуры данных, структуры, которые увеличивают и уменьшают свой размер по требованию и которые являются более гибкими, чем простые массивы, даже массивы, создаваемые и изменяемые динамически с помощью mallocO и reallocO. Массивы при добавлении или удалении новых элементов требуется также повторно сортировать. Одной из таких структур является дерево двоичного поиска, которое мы для краткости будем называть просто «двоичным деревом» («binary tree»). Двоичное дерево хранит элементы в сортированном порядке, вводя их в дерево в нужном месте при их появлении. Поиск по двоичному дереву также осуществляется быстро, время поиска примерно такое же, как при двоичном поиске в массиве. В отличие от массивов, двоичные деревья не нужно каждый раз повторно сортировать с самого начала при добавлении к ним элементов. У двоичных деревьев есть один недостаток. В случае, когда вводимые данные уже отсортированы, время поиска в двоичном дереве сводится ко времени линейного поиска. Техническая сторона этого вопроса должна иметь дело с тем, как двоичные деревья управляются внутренне, что вскоре будет описано. Теперь не избежать некоторой формальной терминологии, относящейся к структурам данных. На рис. 14.1 показано двоичное дерево. В информатике деревья изображаются, начиная сверху и расширяясь вниз. Чем ниже спускаетесь вы по дереву, тем больше его глубина. Каждый объект внутри дерева обозначается как вершина (node). На вершине дерева находится корень дерева с глубиной 0. Внизу находятся концевые вершины различной глубины. Концевые вершины отличают по тому, что у них нет ответвляющихся поддеревьев (subtrees), тогда как у внутренних вершин есть по крайней мере одно поддерево. Вершины с поддеревьями иногда называют родительскими (parent), они содержат порожденные вершины (children). Чистые двоичные деревья отличаются тем, что каждая вершина содержит не более двух порожденных вершин. (Деревья с более чем двумя вершинами полезны, но не существенны для нашего обсуждения.) Порожденные вершины называются в этом случае левой и правой соответственно. Деревья двоичного поиска отличаются еще и тем, что значения, хранящиеся в левой порожденной вершине, всегда меньше значения в родительской вершине, а значения, хранящиеся в правой порожденной вершине, всегда больше значения в родительской вершине. Это предполагает, что внутри дерева нет повторяющихся значений. Этот факт также объясняет, почему деревья не эффективны при работе с предварительно отсортированными данными: в зависимости от порядка сортировки, каждый новый элемент данных сохраняется либо только слева, либо только справа от находящегося впереди него элемента, образуя простой линейный список. К двоичным деревьям применяют следующие операции: Ввод Добавление к дереву нового элемента.
14.4. Расширенный поиск с помощью двоичных деревьев 521 Корень Внутренние и концевые вершины Концевые вершины Рис. 14.1. Двоичное дерево Поиск Нахождение элемента в дереве. Удаление Удаление элемента из дерева. Прохождение (traversal) Осуществление какой-либо операции с каждым хранящимся в дереве элементом. Прохождение дерева называют также обходом дерева (tree walk). Есть разнообразные способы «посещения» хранящихся в дереве элементов. Обсуждаемые здесь функции реализуют лишь один из таких способов. Мы дополнительно расскажем об этом позже. 14.4.2. Функции управления деревьями Только что описанные операции соответствуют следующим функциям: #include <search.h> /* XSI */ void *tsearch(const void *key, void **rootp, int (*compare)(const void *, const void *)); void *tfind(const void *key, const void **rootp, int (*compare)(const void *, const void *)); void *tdelete(const void *key, void **rootp, int (*compare)(const void *, const void *)); typedef enum { preorder, postorder, endorder, leaf } VISIT; void twalk(const void *root, Глубина О i Глубина 1 Глубина 2 Глубина 3 J0. ^^ —Ч 3 J 9 I /\ /V 2 j 4 i ¦7 I 10 i ! *^««?v^e# *тю>>*&<яф bfmxtzg^ -*&^4№f0* J / / \ 1 \ ¦ .6 | 8 | "
522 Глава 14. Расширенные интерфейсы void (*action)(const void *nodep, const VISIT which, const int depth)) ; void tdestroy(void *root, void (*free__node)(void *nodep)); /* GLIBC*/ Эти функции были впервые определены для System V, а теперь формально стандартизованы PQSIX. Они следуют структуре других, которые мы видели в разделе 6.2 «Функции сортировки и поиска»: использование указателей void* для указания на произвольные типы данных и предоставляемые пользователем функции сравнения для определения порядка. Как и для qsor't () и bsearch ()., функции сравнения должны возвращать отрицательное/нулевое/положительное значение, когда key сравнивается со значением в вершине дерева. 14.4.3. Ввод элемента в дерево: tsearchO Эти процедуры выделяют память для вершин дерева. Для их использования с несколькими деревьями нужно предоставить им указатель на переменную void*, в которую они заносят адрес корневой вершины. При создании нового дерева инициализируйте этот указатель в NULL: void *root = NULL; /* Корень нового дерева */ void *val; /* Указатель на возвращенные данные */ extern int my_compare(const void *, const void *) ; /* Функция сравнения */ extern char key[], key2[]; /* Значения для ввода в дерево */ val = tsearch(key, & root, my_compare) ; /* Ввести в дерево первый элемент */ /* ...заполнить кеу2 другим значением. НЕ изменять корень... */ val = tsearch(key2, & root, ray_corapare); /* Ввести в дерево последующий элемент */ Как показано, в переменной root должен быть NULL лишь в первый раз, после чего нужно оставить ее как есть. При каждом последующем вызове tsearch () использует ее для управления деревом. Когда разыскиваемый key найден, как tsearch (), так и tf ind () возвращают указатель на содержащую его вершину. Поведение функций различно, когда key не найден: tf ind () возвращает NULL, a tsearch () вводит в дерево,новое значение и возвращает указатель на него. Функции tsearch () и tf ind () возвращают указатели на внутренние вершины дерева. Они могут использоваться в последующих вызовах в качестве значения root для работы с поддеревьями. Как мы вскоре увидим, значение key может быть указателем на произвольную структуру; он не ограничен символьной строкой, как можно было бы предположить из предыдущего примера. Эти процедуры сохраняют лишь указатели на данные, использующиеся в качестве ключей. Соответственно это ваше дело управлять памятью для хранения значений данных, обычно с помощью malloc (). 13АМЕЧАНИЕ. Поскольку функции деревьев хранят указатели, тщательно поза- Хботьтесь о том, чтобы не использовать realloc() для значений, которые были [использованы в качестве ключей! realloc () может переместить данные, вернув \ новый указатель, но процедуры деревьев все равно сохранят висящие (dangling) ука- Хзатели на старые данные.
14.4. Расширенный поиск с помощью двоичных деревьев 523 14.4.4. Поиск по дереву и использование возвращенного указателя: tf ind () и tsearch () Функции tf ind() и tsearch () осуществляют поиск в двоичном дереве по данному ключу. Они принимают тот же самый набор аргументов: ключ для поиска key; указатель на корень дерева, rootp; и compare, указатель на функцию сравнения. Обе функции возвращают указатель на вершину, которая соответствует key. Как именно использовать указатель, возвращенный tfind() и tsearchO? Во всяком случае, на что именно он указывает? Ответ заключается в том, что он указывает на вершину в дереве. Это внутренний тип; вы не можете увидеть, как он определен. Однако, POSIX гарантирует, что этот указатель может быть приведен к указателю на указа- тель на что бы то ни было, что вы используете в качестве ключа. Вот обрывочный код для демонстрации, а затем мы покажем, как это работает: struct employee { /* Из главы б */ char lastname[30]; char firstname[30]; long emp_id; time_t start_date; }; /* emp_name_id_compare сравнение по имени, затем по ID */ int emp_name_id_compare (const void *elp, const void *e2p) { /* ...также из главы 6, полностью представлено позже... */ } struct employee key = { ... }; void *vp, *root; struct employee *e; /*....заполнение данными... */ ¦ vp = tfind(& key, root, emp_name_id_compare) ; if (vp != NULL) { /* it's there, use it */ e = *( (struct employee **) vp) ; /* Получить хранящиеся в дереве данные */ /* использование данных в *е ... */ } Как можно указатель на вершину использовать как указатель на указатель данных? Рассмотрим, как была бы реализована вершина двоичного дерева. В каждой вершине хранится по крайней мере указатель на элемент данных пользователя и указатели на потенциальные порожденные вершины справа и слева. Поэтому она должна выглядеть примерно так: struct binary_tree { void *user_data; /* Указатель на данные пользователя */ struct binary_tree *left; /* Порожденная вершина слева или NULL */ struct binary_tree *right; /* Порожденная вершина справа или NULL */
524 Глава 14. Расширенные интерфейсы /* ...здесь возможны другие поля..% */ . , } node; С и C++ гарантируют, что поля внутри структуры располагаются в порядке возрастания адресов. Таким образом, выражение '& node. left < & node. right' истинно. Более того, адрес структуры является также адресом ее первого поля (другими словами, игнорируя проблемы типов, '& node == &node.user_data'). Следовательно, концептуально'е = *( (struct employee **) vp);'означает: 1. vp является void *, то есть общим указателем. Это адрес внутренней вершины дерева, но это также адрес части вершины (скорее всего, другого void *), которая указывает на данные пользователя. 2. '(struct employee **) vp'приводит адрес внутреннего указателя к нужному типу; он остается указателем на указатель, но в этот раз на struct employee. Помните, что приведение одного типа указателя к другому не изменяют значения (паттерна битов); оно меняет лишь способ интерпретации компилятором значения для анализа типов. 3. '* ( (struct employee **) vp)' разыменовывает вновь созданный struct employee * *, возвращая годный к употреблению указатель struct employee *. 4. 'е = * ( (struct employee * *) vp)' сохраняет это значение в е для непосредственного использования позже. Идея проиллюстрирована на рис. 14.2. vp 1 fr userjiata |« -тштё ' left I right void ** struct binary_jtree Для упрощения использования возвращенного указателя вы могли бы рассмотреть определение макроса: #define tree__data(ptr, type) (*(type.**) (ptr)) struct employee *e; void *vp; vp = tfind(& key, root, emp__name__id_compare) ; if (vp != NULL) { /* it's there, use it */ e = tree__data (vp, struct employee); /* использование сведений в *е ... */ } struct employee Рис. 14.2. Вершины дерева и их указатели
14.4. Расширенный поиск с помощью двоичных деревьев 525 14.4.5. Обход дерева: twalk () Функция twalk () объявлена в <search. h> следующим образом: typedef enum { preorder, postorder, endorder, leaf } VISIT; void twalk (const void *root, ' -!''"-¦' void (*action)(const void *nodep/ const VISIT which, const int depth)); ¦ Первый параметр является корнем дерева (не указателем на корень). Второй является указателем на функцию обратного вызова, которая вызывается с тремя аргументами: указателем на исследуемую вершину дерева; типом перечисления, указывающим, как осуществляется обход данной вершины; и целого, обозначающего глубину текущей вершины (корень находится на глубине 0, как объяснялось ранее). Использование функции обратного вызова здесь такое же, как для nf tw () (см. раздел 8.4.3.2 «Функция обратного вызова nf tw () »). Там функция обратного вызова вызывается для каждого объекта в файловой системе. Здесь функция обратного вызова вызывается для каждого объекта, хранящегося в дереве. Есть несколько способов прохождения, или «обхода», двоичного дерева: Левая вершина, родительская вершина, правая вершина. Родительская вершина, левая вершина, правая вершина. Левая вершина, правая вершина, родительская вершина. Функция GLIBC twalk () использует второй способ: сначала родительская вершина, затем левая, затем правая. Каждый раз при встрече с вершиной говорят, что она посещается1. В ходе посещения порожденной вершины функция должна посетить и родительскую. Соответственно, значения типа VISIT указывают, на какой стадии произошла встреча с этой вершиной: preorder До посещения порожденных. postorder После посещения первой, но до посещения второй порожденной вершины. endorder После посещения обеих порожденных. leaf Эта вершина является концевой, не имеющей порожденных вершин. ЗАМЕЧАНИЕ. Использованная здесь терминология не соответствует точно тойI, которая используется в формальных руководствах по структурированию данных. Там используются термины inorder, preorder и postorder для обозначения соответствующих трех перечисленных ранее способов прохождения дерева. Таким образом, twalk () использует прохождение по типу preorder, но использует именованные константы preorder и т. д. для обозначения того, на какой стадии была посещена вершина. Это может сбивать с толку. 7 В голову приходят образы, как маленькие двоичные структуры данных сидят друг рядом с другом за чаем и пирожными. По крайней мере, такое бывает, если вы проводите слишком много времени перед своим компьютером... - Примеч. автора.
526 Глава 14. Расширенные интерфейсы Следующая программа, chl4-1 search. с, демонстрирует построение и обход дерева. Она повторно использует структуру struct employee и функцию emp_name_id_compare () из раздела 6.2 «Функции сортировки и поиска». 1 /* chl4-tsearch.с демонстрация управления деревом */ 2 3 #include <stdio.h> 4 #include <search.h> 5 #include <time.h> 6 7 struct employee { 8 char lastname[30]; 9 char firstname[30]; 10 long emp_id; 11 time_t start_date; 12 } ; 13 14 /* emp_name_id_compare сравнение по имени, затем по ID */ 15 16 int emp_name__id_compare (const void *elp, const void *e2p) 17 { 18 const struct employee *el, *e2; 19 int last, first; 20 . 21 el = (const struct employee *) elp; 22 e2 = (const struct employee *) e2p; 23 24 if ((last = strcmp(el->lastname/ e2->lastname)) != 0) 2 5 return last; 26 27 /* фамилии совпадают, проверить имена */ 28 if ((first = strcmp(el->firstname, e2->firstname)) != 0) 29 return first; 30 31 /* имена совпадают, проверить ID */ 32 if (el->emp_id < e2->emp_id) 3 3 return -1; 34 else if (el->emp_id == e2->emp_id) 3 5 return 0; 36 else 37 return 1; 38 } 39 40 /* print_emp вывод структуры employee во время обхода дерева */ 41 42 void print_emp(const void *nodep, const VISIT which, const int depth)
14.4. Расширенный поиск с помощью двоичных деревьев 527 43 { .% 44 struct employee *e = *((struct employee **) nodep); 45 46 switch (which) { 47 case leaf: 48 case postorder: 49 printf("Depth: %d. Employee:\n", depth); 50 printf("\t%s, %s\t%d\t%s\n", e->lastname, e->firstname, 51 e->emp_id, ctime(& e->start_date)); 52 break; 53 default: 54 break; 55 } 56 } ' Строки 7—12 определяют struct employee, а строки 14-38 определяют emp_name_id_compare(). Строки 40-56 определяют print_emp (), функцию обратного вызова, которая выводит struct employee наряду с глубиной дерева в текущей вершине. Обратите внимание на магическое приведение типа в строке 44 для получения указателя на сохраненные данные. 5 8 /* main демонстрация хранения данных в двоичном дереве */ 59 60 int main(void) 61 { 62 #define NPRES 10 63 struct .employee presidents[NPRES] ; 64 int i, npres; 65 char buf[BUFSIZ]; 66 void *root = NULL; 67 68 /* Очень простой код для чтения данных: */ 69 for (npres = 0; npres < NPRES && fgets(buf, BUFSIZ, stdin) != NULL; 70 npres++) { 71 sscanf(buf, *'%s %s %ld %ld\n" , 72 presidents[npres].lastname, 73 presidents[npres].firstname, 74 & presidents[npres].emp_id, 75 & presidents[npres].start_date); 76 } 77 7 8 for (i = 0; i < npres; i++) 79 (void) tsearch(& presidents[i], & root/ emp_name_id_compare) ; 80 81 twalk(root, print_emp); 82 return 0; 83 }
528 Глава 14. Расширенные интерфейсы Целью вывода дерева является вывод содержащихся в нем элементов в отсортированном порядке. Помните, что twalk () посещает промежуточные вершины по три раза и что левая вершина меньше родительской, тогда как правая больше. Таким образом, оператор switch выводит сведения о вершине, лишь если which равно leaf, является концевой вершиной, или postorder, что означает, что была посещена левая вершина, а правая еще не была посещена. Используемые данные представляют собой список президентов, тоже из раздела 6.2 «Функции сортировки и поиска». Чтобы освежить вашу память, полями являются фамилия, имя, номер сотрудника и время начала работы в виде временной отметки в секундах с начала Эпохи: $ cat presdata.txt Bush George 43 980013600 Clinton William 42 727552800 Bush George 41 601322400 Reagan Ronald 40 348861600 Carter James 39 222631200 Данные сортируются на основе сначала фамилии, затем имени, а затем старшинства. При запуске8 программа выдает следующий результат: $ chl4-tsearch < presdata.txt Depth: 1. Employee: Bush, George 41 Fri Jan 20 13:00:00 1989 Depth: 0. Employee: Bush, George 43 Sat Jan 20 13:00:00 2001 Depth: 2. Employee: Carter, James 39 Thu Jan 20 13:00:00 1977 Depth: 1. Employee: Clinton, William 42 Wed Jan 20 13:00:00 1993 Depth: 2. Employee: Reagan, Ronald 40 Tue Jan 20 13:00:00 1981 14.4.6. Удаление вершины дерева и удаление дерева: tdelete () ntdestroyO Наконец, вы можете удалить элементы из дерева и, на системах GLIBC, удалить само дерево целиком: void *tdelete(const void *key, void **rootp, int (^compare)(const void *, const void *)); /* Расширение GLIBC, в POSIX нет: */ void tdestroy (void *root, void (*free_node)(Void *nodep)); Этот вывод для часового пояса U.S. Eastern Time zone. - Примеч. автора.
14.5. Резюме 529 Аргументы для tdelete () те же, что и для tsearch (): ключ, адрес корня дерева и функция сравнения. Если в дереве найден данный элемент, он удаляется, и tdelete () возвращает указатель на родительскую вершину. В противном случае возвращается NULL. С этим поведением следует обращаться в своем коде осмотрительно, если вам нужен первоначальный удаляемый элемент, например, для освобождения занимаемой им памяти: struct employee *e, key; /* Объявления переменных */ void *vp, *root; /* ...заполнить ключ для удаляемого из дерева элемента... *•/ vp = tfind(& key, root, enp_name_id_compare); /* Найти удаляемый элемент */ if (vp != NULL) { e = *((struct employee **) vp); /* Преобразовать указатель */ free(e); /* Освободить память */ } (void) tdelete(& key, & root, errp_name_i6Lcompare); /* Теперь удалить его из дерева */ Хотя это и не указано в справочных страницах или стандарте POSIX, под GNU/Linux, если вы удаляете элемент, хранящийся в корневой вершине, возвращается значение новой корневой вершины. Для переносимого кода не следует полагаться на это поведение. Функция tdestroy () является расширением GLIBC. Она позволяет удалить дерево целиком. Первый аргумент является корнем дерева. Второй является указателем на функцию, которая освобождает данные, на которые указывает каждая вершина дерева. Если с этими данными ничего не надо делать (например, они хранятся в обычном массиве, как в примере нашей программы), эта функция ничего не должна делать. Не передавайте указатель NULL! Это приведет к аварийной ситуации. 14.5. Резюме Иногда бывает необходимо выделить память, выровненную по определенной границе. Это осуществляет posix_memalign(). Ее возвращаемое значение отличается от большинства из рассмотренных в данной книге функций. memalignO также выделяет выровненную память, но не все системы поддерживают освобождение памяти с помощью free (). Блокирование файлов с помощью fcntl() предусматривает блокировку диапазонов, вплоть до блокирования отдельных байтов в файле. Блокировки чтения предотвращают запись в заблокированную область, а блокировки записи предотвращают чтение и запись другими процессами в заблокированную область. По умолчанию используется вспомогательная блокировка, и POSIX стандартизует лишь ' вспомогательную блокировку. Большинство современных систем Unix поддерживают обязательную блокировку, используя для файла бит setgid прав доступа, а также возможные дополнительные опции монтирования файловой системы. В GNU/Linux функция lockf () действует в качестве оболочки вокруг блокировки POSIX с помощью f cntl (); блокировки функции BSD flock () совершенно независимы от блокировок f cntl (). Блокировки BSD flock () используются лишь для
530 Глава 14. Расширенные интерфейсы всего файла в целом и не работают на удаленных файловых системах. По этим причинам использование блокировки flock () не рекомендуется. • gettimeof day () получает время дня в виде пар (секунды, микросекунды) в struct timeval. Эти значения используются utimes () для обновления времени доступа и модификации файла. Системные вызовы get timer () и set timer () используют пары struct timeval в struct itimerval для создания интервальных таймеров - сигнальных часов, которые «срабатывают» в установленное время и продолжают срабатывать впоследствии с заданным интервалом. Три различных таймера обеспечивают контроль тех состояний, когда таймер продолжает действовать. Функция nanosleep () использует struct timespec, которая указывает время в секундах и наносекундах, чтобы приостановить выполнение процесса в течение определенного интервала времени. У нее есть удачная особенность не взаимодействовать вообще с механизмами сигналов. Три API являются стандартным набором функций для хранения и поиска данных, которые сохраняют данные в двоичных деревьях в отсортированном виде. Эти три API очень гибкие, позволяя использовать множество деревьев и произвольные данные. Упражнения 1. Напишите функцию lockf (), используя f cntl () для осуществления блокировки. 2. Каталог /usr/src/linux/Documentation содержит набор файлов, которые описывают различные аспекты поведения операционной системы. Прочитайте файлы locks.txt и mandatory.txt, чтобы получить больше сведений об обработке Linux блокировок файлов. 3. Запустите на своей системе программу chl4-lockall без обязательной блокировки и посмотрите, сможете ли изменить файл-операнд. 4. Если у вас не-Linux система, поддерживающая обязательную блокировку, попробуйте исполнить на ней программу chl4-lockall. 5. Напишите функцию strf times () следующего вида: size_t strftimes(char *buf, size_t size, const char *format, const struct timeval *tp); Она должна вести себя подобно стандартной функции strf time О за тем исключением, что должна использовать %q для обозначения «текущего числа микросекунд». 6. Используя только что написанную функцию strf times (), напишите расширенную версию date, которая принимает форматирующую строку, начинающуюся с ведущего +, и форматирует текущие дату и время (см*. date{\)). 7. Обработка тайм-аута в chl4-timers .с довольно примитивна. Перепишите программу с использованием setjmpO после вывода приглашения и longjmpO из обработчика сигнала. Улучшает ли это структуру или ясность программы? 8. Мы заметили, что chl4-timers . с содержит намеренное состояние гонки. Предположим, пользователь вводит ответ в нужное время, но chl4-timers приостановлена, прежде чем сигнал может быть отменен. Какой вызов вы сделаете, чтобы уменьшить размер проблемного окна?
Упражнения 531 9. Нарисуйте дерево, как показано в выводе chl4-tsearch в разделе 14.4.5 «Обход дерева: twalk()». 10.Исследуйте файл /usr/share/dict/words на системе GNU/Linux. (Это словарь проверки правописания для spell; на различных системах он может находиться в разных местах.) В файле слова размещены в отсортированном порядке, по одному в строке. Для начала используйте программу awk для создания нового списка в случайном порядке: $ awk Ч list[$0]++ } > END { for (i in list) print i }' /usr/share/dict/words > /txnp/wlist Далее, напишите две программы. Каждая должна читать новый список и сохранять каждое прочитанное слово в дереве и массиве соответственно. Вторая программа должна использовать для сортировки массива qsort (), а для поиска - bsearch (). Получите из дерева или массива слово 'gravy'. Вычислите время работы двух программ, чтобы увидеть, какая быстрее. Вам может потребоваться заключить получение слова внутрь цикла, повторяющегося множество раз (скажем, 1000), чтобы получить достаточное для определения разницы время. Используйте вывод ps, чтобы посмотреть, сколько памяти используют программы. 11. Повторно запустите обе программы, использовав оригинальный отсортированный словарный файл, и посмотрите, как изменятся временные результаты (если они вообще изменятся).
Часть 3 Отладка и заключительный проект Глава 15. Отладка 533 Глава 16. Проект, связывающий все воедино - -601
Глава 15 Отладка В этоц главе: • 15.1. Сначала главное 534 • 15,2. Компиляция для отладки 534 » 15.3. Основы GDB . . . . 535 \ 15.4, Программирование для отладки 542 • 15 5..Отладочные инструменты 568 • 15.6. Тестирование программ 593 • 15.7. Правила отладки , 595 • 15.8. Рекомендуемая литература 597 • 15.9. Резюме . . 598 • Упражнения • • • 599 Ахмеется множество правил, начиная с логики программы и расположения данных, через организацию и расположение кода и кончая реализацией, которые могут минимизировать ошибки и проблемы. Мы рекомендуем вам изучить их; найдите хорошие книги по проектированию и дизайну программного обеспечения и реализуйте содержащиеся там советы на практике! Каждая программа, размером превышающая несколько сот строк кода, должна быть тщательно продумана и спроектирована, а не обтяпана, пока не начнет работать. Однако, поскольку программисты люди, ошибки программирования неизбежны. Отладка является процессом обнаружения и устранения ошибок в программах. Даже хорошо спроектированные и хорошо реализованные программы иногда не работают; когда что-то идет не так и вы не можете выяснить, почему, хорошей мыслью является нацелить на код отладчик и понаблюдать за появлением ошибки. Данная глава охватывает ряд тем, начиная с общих методик и советов по отладке (компилирование для отладки и элементарное использование GDB,-отладчика GNU), переходя к ряду методик для использования при разработке и отладке программы, упрощающих отладку, и затем рассмотрением ряда инструментов, помогающих в процессе отладки. Глава завершается краткими сведениями по тестированию программного обеспечения и великолепным набором «правил отладки», извлеченных из книги, которую мы весьма рекомендуем. Большая часть наших советов основана на нашем долгосрочном опыте участия в качестве добровольца в проекте GNU по поддержке gawk (GNU awk). Большинство, если не все, специфические примеры, которые мы представляем, происходят от этой программы. На протяжении главы особые рекомендации помечены словом Рекомендация.
534 Глава 15. Отладка 15.1. Сначала главное Когда программа ведет себя неправильно, вы можете быть в затруднении, что делать сначала. Часто странное поведение возникает из-за неправильного использования памяти - использования неинициализированных значений, чтения или записи за пределами динамической памяти и т. д. Поэтому вы можете быстрее получить результаты, попробовав сред- ства отладки памяти до того, как заведете отладчик. Довод заключается в том, что утилиты памяти могут указать вам непосредственно на вызывающую сбой строку кода, тогда как использование отладчика больше напоминает миссию «найти и уничтожить», в которой вам нужно сначала изолировать проблему, а затем исправить ее. Убедившись, что дело не в проблемах памяти, можно переходить к использованию отладчика. Поскольку отладчик является более универсальным средством, мы рассмотрим его вначале. Далее в главе мы обсудим ряд инструментов для отладки памяти. 15.2. Компиляция для отладки Для использования отладчика исходного кода, отлаживаемый исполняемый файл должен быть откомпилирован с опцией компилятора -д. Эта опция заставляет компилятор внедрять в объектный код дополнительные отладочные идентификаторы', то есть дополнительные сведения, содержащие имена и типы переменных, констант, функций и так далее. Отладчик затем использует эту информацию для приведения в соответствие местоположения исходного кода с исполняемым кодом и получения или сохранения значений переменных в работающей программе. На многих системах Unix опция компилятора -д является взаимно исключающей с опцией -О, которая включает оптимизацию. Это потому, что оптимизации могут вызвать перестановку битов и участков объектного кода, так что больше не будет прямого соответствия с тем, что исполняется, и линейным прочтением исходного кода. Отменив оптимизации, вы значительно облегчаете отладчику установление связи между объектным и исходным кодом, и в свою очередь, пошаговое прохождение программы работает очевидным образом. (Пошаговое исполнение вскоре будет описано.) GCC, GNU Compiler Collection (коллекция компиляторов GNU), на самом деле допускает совместное использование -д и -О. Однако, это привносит как раз ту проблему, которую мы хотим избежать при отладке: следование исполнению в отладчике становится значительно более трудным. Преимуществом совместного использования опций является то, что вы можете оставить отладочные идентификаторы в конечном оптимизированном исполняемом модуле. Они занимают лишь дисковое пространство, а не память. После этого установленный исполняемый файл все еще можно отлаживать при непредвиденных случаях. По нашему опыту, если нужно использовать отладчик, лучше перекомпилировать приложение с самого начала, использовав лишь опцию -д. Это значительно упрощает
15.3. Основы GDB 535 трассировку; имеется достаточно деталей, за которыми нужно следить при простом прохождении написанной программы, не беспокоясь о том, как компилятор переставляет код. Есть одно предостережение: убедитесь, что поведение программы все еще неправильное. Воспроизводимость является ключевой при отладке; если вы не можете воспроизвести проблему, гораздо труднее ее выследить и исправить. В редких случаях компиляция без опции -О может устранить ошибку1. Обычно проблема остается при компиляции без использования опции -О, что означает, что на самом деле действительно имеется какая-то разновидность логической ошибки, ждущая своего обнаружения. 15.3. Основы GDB Отладчик является программой, позволяющей контролировать исполнение другой программы и исследовать и изменять состояние подчиненной программы (такое, как значения переменных). Имеются два вида отладчиков: отладчики машинного уровня, работающие на уровне машинных инструкций, и отладчики исходного кода, работающие на основе исходного кода программы. Например, в отладчике машинного уровня для изменения значения переменной вы указываете адрес в памяти. В отладчике исходного уровня вы просто используете имя переменной. Исторически в V7 Unix был adb, который являлся отладчиком машинного уровня. В System III был sdb, который являлся отладчиком исходного кода, a BDS Unix предоставляла dbx, также отладчик исходного кода. (Обе продолжали предоставлять adb.) dbx продолжает .существовать на некоторых коммерческих системах Unix. GDB, отладчик GNU, является отладчиком исходного кода. У него значительно больше возможностей, он значительно более переносим и более практичен, чем любой из sdb или dbx2. Как и его предшественники, GDB является отладчиком командной строки. Он выводит по одной строке исходного кода за раз, выдает приглашение и читает одну строку ввода, содержащего команду для исполнения. Имеются графические отладчики; они предоставляют больший обзор исходного кода и обычно предоставляют возможность манипулировать программой как из окна командной строки, так и через компоненты GUI, такие, как кнопки и меню. Отладчик ddd является одним из таких; он построен поверх GDB, так что если вы изучите GDB, вы сразу же сможете Оптимизации компилятора являются общеизвестным козлом отпущения для логических ошибок. В прошлом обвинения компиляторов были более оправданы. Судя по нашему опыту, используя современные системы и компиляторы, очень редко можно обнаружить случаи, в которых оптимизации компилятора привносят ошибки в работающий код. - Примеч. автора. Мы говорим об оригинальном BSD dbx. В течение десяти лет мы использовали исключительно GDB. - Примеч. автора. ddd поставляется со многими системами GNU/Linux. Исходный код доступен на FTP-сайте проекта GNU ddd (f tp: / / f tp. gnu. org/gnu/ddd/) - Примеч. автора.
536 Глава 15. Отладка начать использовать ddd. (У ddd есть собственное руководство, которое следует прочесть, если вы собираетесь интенсивно его использовать.) Другим графическим отладчиком является Insight4, который использует для предоставления поверх GDB графического интерфейса Tcl/Tk. (Следует использовать графический отладчик, если он доступен и нравится вам. Поскольку мы собираемся предоставить введение в отладчики и отладку, мы выбрали использование простого интерфейса, который можно представить в напечатанном виде.) GDB понимает С и C++, включая поддержку восстановления имен (name deman- gling), что означает, что вы можете использовать для функций-членов классов и перегруженных функций обычные имена исходного кода C++. В частности, GDB распознает синтаксис выражений С, что полезно при проверке значения сложных выражении, таких, как 4*ptr->x. а [ 1] ->q\ Он понимает также Fortran 77, хотя вам может понадобиться добавить к имени функции или переменной Фортрана символ подчеркивания. GDB также частично поддерживает Modula-2 и имеет ограниченную поддержку Паскаля. Если вы работаете на системе GNU/Linux или BSD (и установили средства разработки), у вас, вероятно, уже установлена готовая к использованию последняя версия GDB. Если нет, исходный код GDB можно загрузить с FTP-сайта проекта GNU для GDB5 и самостоятельно его построить. GDB поставляется с собственным руководством, которое занимает 300 страниц. В каталоге исходного кода GDB можно сгенерировать печатную версию руководства и самостоятельно его распечатать. Можно также купить в Free Software Foundation (FSF) готовые печатные экземпляры; ваша покупка поможет FSF и непосредственно внесет вклад в производство большего количества свободного программного обеспечения. (Информацию для заказа см. на веб-сайте FSFN. Данный раздел описывает лишь основы GDB; мы рекомендуем прочесть руководство, чтобы научиться использовать все преимущества возможностей GDB. 15.3.1. Запуск GDB Основное использование следующее: gdb [ опции ] [ исполняемый файл [имя файла дампа ] ] Здесь исполняемый файл является отлаживаемой программой. Имя файла дампа, если оно имеется, является именем файла core, созданном при завершении программы операционной системой с созданием снимка процесса. Под GNU/Linux такие файлы (по умолчанию) называются core.pid7, где pid является ID процесса запущенной программы, которая была завершена. Расширение pid означает, что в одном каталоге могут находиться несколько дампов ядра, что бывает полезно, но также занимает дисковое пространство! 4 http : / /sources . redhat. com/insight/ - Примеч. автора. 5 ftp: / /, f tp. gnu .org/ gnu / gdb / - Примеч. автора. 6 ht tp: / /www. gnu .org - Примеч. автора. 7 Если вы хотите изменить такое поведение, см. sysctl(%). - Примеч. автора.
15.3. Основы GDB 537 Если вы забыли указать в командной строке имена файлов, для сообщения GDB имени исполняемого файла можно использовать 'file исполняемый-файл", а для имени файла дампа- 'core-file имя-файла-дампа'. При наличии дампа ядра GDB указывает место завершения программы. Следующая программа, chl5-abort .с, делает несколько вложенных вызовов функций, а затем намеренно завершается посредством abort (), чтобы создать дамп ядра: /* chl5-abort.c создает дамп ядра */ . #include <stdio.h> #include <stdlib.h> /* recurse создание нескольких вызовов функций */ void recurse(void) { static int i; if (++i ==. 3) abort(); else recurse() ; } int main(int argc, char **argv) { recurse(); } Вот небольшой сеанс GDB с этой программой: $ gcc -g chl5-abort.c -о chl5-abort /* Компилировать без -О */ $ chl5-abort /* Запустить программу */ Aborted (core dumped) /* Она печально завершается */ $ gdb chl5-abort core.4124 /* Запустить для нее GDB */ GNU gdb 5.3 Copyright 2002 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 "i686-pc-linux-gnu"... Core was generated by vchl5-abort'. ' Program terminated with signal 6, Aborted. Reading symbols from /lib/i686/libc.so.6...done. Loaded symbols for /lib/i686/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2
538 Глава 15. Отладка #0 0x42028ccl in kill () from /lib/i-686/libc. so. 6 (gdb) where /* Вывести трассировку стека */¦ #0 0x42028ccl in kill () from /lib/i686/libc.so.6 #1 0x42028ac8 in raise () from /lib/i686/libc.so.6 #2 0x4202a019 in abort () from /lib/i686/libc.so.6 #3 0x08048342 in recurse () at chl5-abort.c:13 /* < Нам нужно исследовать здесь */ #4 0x08048347 in recurse () at chl5-abort.с:15 #5 0x08048347 in recurse () at chl5-abort.c:15 #6 0x0804835f in main (argc=l, argv=0xbffff8f4) at chl5-abort.c:20 #7 0x420158d4 in libc_start_main () from /lib/i686/libc.so.6 Команда where выводит трассировку стека, то есть список всех вызванных функций, начиная с самых недавних. Обратите внимание, что имеется три вызова функции recurse (). Команда* bt, означающая 'back trace' (обратная трассировка), является другим названием для where; ее легче набирать. Вызов каждой функции в стеке называется фреймом. Этот термин пришел из области компиляторов, в которой параметры, локальные переменные и адресы возврата каждой функции, сгруппированные в стеке, называются фреймом стека. Команда frame GDB дает вам возможность исследовать определенный фрейм. В данном случае нам нужен фрейм 3. Это последний вызов recurse (), который вызвал abort (): (gdb) frame 3 /* Переместиться в фрейм 3 */ #3 0x08048342 in recurse () at chl5-abort.с:13 13 abort(); /* GDB выводит в фрейме положение в исходном коде */ (gdb) list /* Показать несколько строк исходного кода */ 8 void recurse(void) 9 i 10 static int i; 11 12 if (++i == 3) 13 abort(); 14 else 15 recurse() ; 16 } 17 (gdb) /* Нажатие ENTER повторяет последнюю команду .*/ 18 int main(int argc, char **argv) 19 { 2 0 recurse() ; 21 } (gdb) quit /* Выйти из отладчика (пока) */ Как показано, нажатие ENTER повторяет последнюю команду, в данном случае list, для отображения строк исходного кода. Это простой способ прохождения исходного кода.
15.3. Основы GDB 539 Для редактирования командной строки GDB использует библиотеку readline, поэтому для повторения и редактирования ранее введенных команд можно использовать команды Emacs или vi. Оболочка Bash использует ту же самую библиотеку, поэтому если вам более знакомо редактирование командной строки в приглашении оболочки, GDB работает таким же образом. Эта особенность дает возможность избежать утомительного ручного ввода. 15.3.2. Установка контрольных точек, пошаговое выполнение и отслеживаемые точки Часто при ошибках программ создается дамп ядра. Первым шагом является использование GDB с файлом core для определения процедуры, в которой произошло завершение программы. Если оригинальный двоичный файл не был откомпилирован для отладки (т. е. без -д), все, что может сообщить GDB, это имя функции, но больше никаких деталей. Следующим шагом является перекомпилирование программы с возможностью отладки и без оптимизации, а также проверка того, что она все еще содержит ошибку. Предположив, что это так, можно запустить программу под контролем отладчика и установить контрольную точку в процедуре, вызывающей ошибку. Контрольная точка (breakpoint) является точкой, в которой исполнение должно прерваться, остановиться. Контрольные точки можно установить по имени функции, номеру строки исходного файла, файлу исходного файла совместно с номером строки, а также другими способами. После установки контрольной точки программа запускается с использованием команды run, за которой могут следовать аргументы командной строки, которые должны быть переданы отлаживаемой программе. (GDB удобным образом запоминает за вас аргументы; если нужно снова запустить программу с начала, все что нужно - это напечатать лишь саму команду run, и GDB запустит новую копию с теми же аргументами, как и ранее.) Вот короткий сеанс с использованием gawk: $ gdb gawk /* Запуск GDB для gawk */ GNU gdb 5.3 (gdb) break do,print /* Прерывание в do_print */ Breakpoint 1 at 0x805a36a: file builtin.c, line 1504. (gdb) run •BEGIN { print "hello, world" }¦ /* Запуск программы */ Starting program: /home/arnold/Gnu/gawk/gawk-3.1.3/gawk 'BEGIN { print "hello, world" }' Breakpoint 1, do_print (tree=0x8095290) at builtin.c:1504. 1504 struct redirect *rp = NULL; /* Исполнение достигает контрольной точки */ (gdb) list /* Показать исходный код */ 1499 1500 void 1501 do_print(register NODE *tree)
540 Глава 15. Отладка 1502 { 1503 register NODE **t; 1504 struct redirect *rp ¦= NULL; 1505 register FILE *fp; 1506 int numnodes, i; 1507 NODE *save; 1508 NODE *tval; По достижении контрольной точки вы проходите программу в пошаговом режиме. Это означает, что GDB разрешает программе исполнять лишь по одному оператору исходного кода за раз. GDB выводит строку, которую собирается выполнить, и выводит приглашение. Чтобы выполнить оператор, используется команда next: (gdb) next /* Выполнить текущий оператор (строка 1504 выше) */ 1510 fp = redirect_to_fp(tree->rnode, & rp); /* GDB выводит следующий оператор */ (gdb) /.* Нажмите ENTER для его выполнения и перехода к следующему */ 1511 if (fp == NULL) (gdb) /* снова ENTER */ 1519 save = tree = tree->lnode; (gdb) /* И снова */ 152 0 for (numnodes = 0; tree != NULL; tree = tree->rnode) Команда step является альтернативной командой для пошагового исполнения. Между next и step есть важное различие, next выполняет следующий оператор. Если этот оператор содержит вызов функции, эта функция вызывается и возвращается до того, как GDB вернет себе управление от работающей программы. С другой стороны, когда вы используете с содержащим вызов функции оператором step, GDB входит в вызываемую функцию, позволяя вам продолжить пошаговое исполнение (или трассировку) программы. Если оператор не содержит вызов функции, step аналогична next. ХЗАМЕЧАНИЕ. Легко забыть, какая команда была использована, и продолжать Хнажимать ENTER для выполнения последующих операторов. Если вы используете I step, вы случайно можете войти в библиотечную функцию, такую как strlen () Хили print f (), с которой на самом деле не хотите возиться. В таком случае Iможно использовать команду finish, которая вызывает исполнение программы \до возврата из текущей функции. Вывести содержимое памяти можно с использованием команды print. GDB распознает синтаксис выражений С, что упрощает и делает естественным проверку структур, на которые ссылаются указатели: (gdb) print *save /* Вывести структуру, на которую указывает save *¦/ $1 = {sub = {nodep = {1 = {lptr = 0x8095250, paran\_name = 0x8095250 "pR\t\b", 11 = 134828624}, r = {rptr = 0x0, pptr = 0, preg =0x0, hd = 0x0, av = 0x0, r_ent = 0}, x = {extra = 0x0, xl = 0, param_list = 0x0}, name = 0x0, number = 1, reflags = 0}, val = { fltnum = 6.6614191194446594e-316, sp = 0x0, slen = 0, sref = 1,
15.3. Основы GDB 541 idx = 0}/ hash -= {next = 0x8095250, name = 0x0, length = 0, value = 0x0, ref =1}}, type = Node_expression_list, flags =.1} В заключение, команда cont (continue - продолжить) дает возможность продолжить выполнение программы. Она будет выполняться до следующей контрольной точки или до нормального завершения, если других контрольных точек нет. Этот пример продолжается с того места, на котором остановился предыдущий: 152 0 for (numnodes = 0; tree != NULL; tree = tree->rnode) (gdb) cont /* Продолжить */ Continuing, hello, world Program exited normally. /* Сообщение от GDB */ (gdb) quit /* Выйти из отладчика */ Отслеживаемая точка (watchpoint) подобна контрольной точке, но используется для данных, а не для кода. Отслеживаемые точки устанавливаются для переменной (или поля структуры или объединения или элемента массива), при их изменении GDB посылает уведомления. GDB проверяет значение отслеживаемой точки по мере пошагового исполнения программы и останавливается при изменении значения. Например, переменная do__lint_old в gawk равна true, когда была использована опция --lint__old. Эта переменная устанавливается в true функцией getopt_long (). (Мы рассмотрели getopt_long () в разделе 2.1.2 «Длинные опции GNU»). В файле main. с программы gawk: int do__lint_old = FALSE; /* предупредить о материале, не имеющейся в V7 awk */ static const struct option optabf] = { { "lint-old", no_argument, & do_lint_old, 1 }, }; Вот пример сеанса, показывающего отслеживаемую точку в действии: $ gdb gawk /* Запустить GDB с gawk */ - GNU gdb 5.3 (gdb) watch do_lint_old /* Установить отслеживаемую точку для переменной */ Hardware watchpoint 1: do_lint_old (gdb) run —lint-old 'BEGIN { print "hello, world" }• /* Запустить программу */ Starting program: /home/arnold/Gnu/gawk/gawk-3.1.4/gawk --lint-old 'BEGIN { print "hello, world" }' Hardware watchpoint 1: do_lint__old Hardware watchpoint 1: do_lint__old Hardware watchpoint 1: do_lint_old /* ¦ Преверса отслекива*е>дэй точки при работе программы */ Hardware watchpoint l: do_lint_old Hardware watchpoint 1: do_lint_old Old value = 0 /* Отслеживаемая точка останавливает программу */
542 Глава 15. Отладка New value = 1 0х420с4219 in _getopt_internal () from /lib/i686/libc.so.6 (gdb) where /* Трассировка стека */ #0 0x420c4219 in _getopt_internal () from /lib/i'686/libc. so. 6 #1 0x420c4e83 in getopt_long () from /lib/i686/libc.so.6 #2 0x080683al in main (argc=3, argv=0xbffff8a4) at main.c:293 #3 0x420158d4 in libc_start_main () from /lib/i686/libc.so.6 (gdb) quit /* На данный момент мы закончили */ The program is running. Exit anyway? (y or n) у /* Да */ GDB может делать гораздо больше, чем мы здесь показали. Хотя руководство GDB большое, его стоит прочесть целиком хотя бы один раз, чтобы ознакомиться с его командами и возможностями. После этого, возможно, будет достаточно просмотреть файл NEWS в каждом новом дистрибутиве GDB, чтобы узнать, что нового или что изменилось. Стоит также распечатать справочную карточку GDB, которая поставляется в дистрибутиве GDB в файле gdb/doc/ref card. tex. Создать печатную версию справочной карточки для PostScript после извлечения исходника и запуска configure можно с помощью следующих команд: $ cd gdb/doc /* Перейти в подкаталог doc */ $ make refcard.ps /* Отформатировать справочную карточку */ Предполагается, что справочная карточка будет распечатана с двух сторон листа бумаги 8,5x11 дюймов8 (размер «letter») в горизонтальном (landscape) формате. В ней на шести колонках предоставлена сводка наиболее полезных команд GDB. Мы рекомендуем распечатать ее и поместить под своей клавиатурой при работе с GDB. 15.4. Программирование для отладки Имеется множество методик для упрощения отладки исходного кода, от простых до сложных. В данном разделе мы рассмотрим ряд из них. 15.4.1. Код отладки времени компилирования Несколько методик относятся к самому исходному коду. 15.4.1.1. Использование отладочных'макросов Возможно, простейшей методикой времени компилирования является использование препроцессора для создания условно компилируемого кода. Например: #ifdef DEBUG fprintf(stderr, "myvar = %d\n", myvar); , fflush(stderr); #endif /* DEBUG */ Примерно 213x275 мм. - Примеч. nepee.
15.4. Программирование для отладки 543 Добавление -DDEBUG к командной строке компилятора вызывает fprintf О при выполнении программы. Рекомендация: сообщения отладки посылайте в stderr, чтобы они не были потеряны в канале и чтобы их можно было перехватить при помощи перенаправления ввода/вывода. Убедитесь, что использовали f flush (), чтобы сообщения были выведены как можно скорее. ХЗАМЕЧАНИЕ. Идентификатор DEBUG, хотя он и очевидный, также часто I злоупотребляется. Лучшей мыслью является использование специфического для вашей {программы идентификатора, такого как MYAPPDEBUG. Можно даже использовать {различные идентификаторы для отладки кода в различных частях программы, таких, I как файловый ввод/вывод, верификация данных, управление памятью и т. д. Разбрасывание больших количеств операторов #if def по всему коду быстро становится утомительным. Большое количество # if'def скрывают также логику программы. Должен быть лучший способ, и в самом деле, часто используется методика с условным определением специального макроса для вывода: /* МЕТОДИКА 1 обычно используемая, но не рекомендуемая, см. текст */ /* В заголовочном файле приложения: */ #ifdef MYAPPDEBUG #define DPRINTO(msg) fprintf(stderr, msg) #define DPRINTl(msg, vl) fprintf(stderr, msg, vl) #define DPRINT2(msg, vl, v2) fprintf(stderr, msg, vl, v2) #define DPRINT3 (msg, vl, v2, v3) fprintf (stderr, msg, vl, v2, v3) #else /* ! MYAPPDEBUG */ #define DPRINTO(msg) #define DPRINT1(msg, vl) #define DPRINT2(msg, vl, v2) #define DPRINT3(msg, vl, v2, v3) #endif /* ! MYAPPDEBUG */ "/* В исходном файле приложения: */ DPRINT1("myvar = %d\n", myvar); DPRINT2("vl = %d, v2 = %f\n", vl, v2); Имеется несколько макросов, по одному на каждый имеющийся аргумент, число которых определяете вы сами. Когда определен MYAPPDEBUG, вызовы макросов DPRINTx() развертываются в вызовы fprintf (). Когда MYAPPDEBUG не определен, эти вызовы развертываются в ничто. (Так, в сущности, работает assert (); мы описали assert () в разделе 12.1 «Операторы проверки: assert () ».) Эта методика работает; мы сами ее использовали и видели, как ее рекомендуют в учебниках. Однако, она может быть усовершенствована и дальше с уменьшением количества макросов до одного: /* МЕТОДИКА 2 наиболее переносима; рекомендуется */ /* В заголовочном файле приложения: */
544 Глава 15. Отладка #ifdef MYAPPDEBUG #define DPRINT(stuff) fprintf stuff #else #define DPRI'NT (stuff) #endif /* В исходном файле приложения: */ DPRINT ((stder r, "inyvar = %d\n", imyvar)); /* Обратите внимание на двойные скобки */ Обратите внимание на то, как макрос извлекается с двумя наборами скобок! Поместив весь список аргументов для fprintf () в один аргумент, вам больше не нужно определять произвольное число отладочных макросов. Если вы используете компилятор, удовлетворяющий стандарту С 1999 г., у вас есть дополнительный выбор, который дает наиболее чистый отладочный код: /* МЕТОДИКА 3 самая чистая, но только для С99 */ /* В заголовочном файле приложения: */ #ifdef MYAPPDEBUG #define DPRINT(mesg, ...) fprintf(stderr, mesg, _VA_ARGS ) #else tdefine DPRINT(me^g, ...) #endif /* В исходном файле приложения: */ DPRINT ("inyvar = %d\n" , myvar) ; DPRINT("vl = %d, v2 = %f\n", vl, v2) ; Стандарт С 1999 г. предусматривает варьирующий макрос (variadic macros); т. е. макрос, который может принимать переменное число аргументов. (Это похоже на варьирующую функцию, наподобие print f ().) В макроопределении три точки '. . .' означают, что будет ноль или более аргументов. В теле макроса специальный идентификатор ' VA ARGS замещается предусмотренными аргументами, сколько бы их ни было. Преимуществом этого механизма является то, что при извлечении отладочного макроса необходим лишь один набор скобок, что делает чтение кода значительно более естественным. Это также сохраняет возможность использовать всего одно имя макроса вместо нескольких, которые меняются в соответствии с числом аргументов. Недостатком является то, что компиляторы С99 пока еще доступны не так широко, что снижает переносимость этой конструкции. (Однако, эта ситуация будет со временем улучшаться.) Рекомендация: Текущие версии GCC поддерживают варьирующие макросы. Таким образом, если вы знаете, что никогда не будете использовать для компилирования своих программ что-то, кроме GCC (или какого-нибудь другого компилятора С99), можете использовать механизм С99. Однако, на момент написания, компиляторы С99 все еще не являются обычным явлением. Поэтому, если ваш код должен компилироваться разными компиляторами, следует использовать макрос в стиле с двумя парами скобок.
15.4. Программирование для отладки 545 15.4.1.2. По возможности избегайте макросов с выражениями В общем, макросы препроцессора С являются довольно острой палкой с двумя концами. Они предоставляют вам большую мощь, но также и большую возможность пораниться самому9. Обычно для эффективности или ясности можно видеть такие макросы: #de?ine RS_is_null (RS_node->var_value == Nnull_string) if (RS_is_null |j today == TUESDAY) ... На первый взгляд, он выглядит замечательно. Условие 'RS__is_null' ясно и просто для понимания и абстрагирует внутренние детали проверки. Проблема возникает, когда вы пытаетесь вывести значение в GDB: (gdb) print RS_is_null No symbol "RS_is_null" in current context. В таком случае нужно разыскать определение макроса и вывести развернутое значение. Рекомендация: Для представления важных условий в своей программе используйте переменные, значения которых при изменении условий явным образом меняется в коде. Вот сокращенный пример из io. с в дистрибутиве gawk: void set_RS О { RS_is_null = FALSE; if (RS->stlen == 0) { RS_is_null = TRUE; matchrec = rsnullscan; } } После установки и сохранения RS_is_null ее можно протестировать в коде и вывести из-под отладчика. ХЗЛМЕЧАНИЕ. Начиная с GCC 3.1 и версии 5 GDB, если вы компилируете свою программу с опциями -gdwarf-2 и -дЗ, вы можете использовать макросы из-под IGDB. В руководстве по GDB утверждается, что разработчики GDB надеются най- \ти в конце концов более компактное представление для макросов, и что опция -дЗ I будет отнесена к группе -д. I Однако, использовать макросы таким способом позволяет лишь комбинация GCC, IGDB и специальных опций: если вы не используете GCC (или если вы используете I бол ее старую версию), у вас все еще есть проблема. Мы придерживаемся своей рекомендации избегать по возможности таких макросов. Bjarne Stroustrup, создатель C++, настойчиво работал над тем, чтобы сделать использование препроцессора С совершенно ненужным в C++. По нашему мнению, он не вполне добился успеха: #include нужен до сих пор, но не обычные макросы. Для С препроцессор остается ценным инструментом, но он должен использоваться благоразумно. - Примеч. автора. 18 -159
546 Глава 15. Отладка Проблема с макросами распространяется также и на фрагменты кода. Если макрос определяет несколько операторов, вы не можете установить контрольную точку в середине макроса. Это верно также для inline-функций С99 и C++: если компилятор заменяет тело inline-функций сгенерированным кодом, снова невозможно или трудно установить внутри него контрольную точку. Это имеет связь с нашим советом компилировать лишь с одной опцией -д; в этом случае компиляторы обычно не используют inline-функций. Обычно с такими строками используется переменная, представляющая определенное состояние. Довольно просто, и это рекомендуется многими книгами по программированию на С, определять с помощью #def ine для таких состояний именованные константы. Например: / * Различные состояния, в которых можно находиться при поиске конца записи. * / #define NOSTATE 1 /* сканирование еще не началось (все) */ #define INLEADER 2 /* пропуск начальных данных (RS ="")*/ #define INDATA 3 /* в теле записи (все) */ #define INTERM 4 /* терминатор сканирования (RS = "", RS = regexp) */ int state; state = NOSTATE; State = INLEADER;. if (state !- INTERM) .. . . На уровне исходного кода это выглядит замечательно. Но опять-таки, есть проблема, когда вы пытаетесь просмотреть код из GDB: (gdb) print state $1 = 2 Здесь вы также вынуждены возвращаться обратно и смотреть в заголовочный файл, чтобы выяснить, что означает 2. Какова же альтернатива? Рекомендация: Для определения именованных констант используйте вместо макросов перечисления (enum). Использование исходного кода такое же, а значения enum может выводить также и отладчик. Пример, тоже из io. с в gawk: typedef enum scanstate { NOSTATE, /* сканирование еще не начато (все) */ INLEADER, /* пропуск начальных данных (RS = "") */ INDATA, /* в теле записи (все) */ INTERM, /* терминатор сканирования (RS = "", RS = regexp) */ } SCANSTATE; SCANSTATE state; /* ... остальной код без изменений! ... */ Теперь при просмотре state из GDB мы видим что-то полезное: (gdb) print state $1 = NOSTATE
15.4. Программирование для отладки 547 15.4.1.3. При необходимости переставляйте код Довольно часто условие в if или while состоит из нескольких проверок, разделенных ScSc или | |. Если эти проверки являются вызовами функций (или даже не являются ими), невозможно осуществить пошаговое прохождение каждой отдельной части условия. Команды GDB step и next работают на основе операторов (statements), а не выражений (expressions). (Разнесение их по нескольким строкам все равно не помогает.) Рекомендация: перепишите исходный код, явно используя временные переменные, в которых сохраняются значения или условные результаты, так что вы можете провсрлть их в отладчике. Первоначальный код должен быть сохранен в комментарии, чтобы вы (или программист после вас) могли сказать, что происходит. Вот конкретный пример: функция do_input () из файла io. с gawk: 1 /* do_input главный цикл обработки ввода */ 2 3 void 4 do_input() ^ 5 { 6 IOBUF *iop; 7 extern int exiting; 8 int rvall, rval2, rval3; 9 10 (void) setjmp(filebuf) ; /* for xnextfile' */. 11 12 while ((iop = nextfile(FALSE)) != NULL) { 13 /* 14 * Здесь было: 15 if (inrec(iop) == 0) 16 while (interpret(expression_value) && inrec(iop) ==0) 17 continue; 18 * Теперь развернуто для простоты отладки. 19 .¦*/ 20 rvall = inrec(iop); 21 if (rvall == 0) { 22 for (;;) { 23 rval2 = rval3 = -1; /* для отладки */ 24 * rval2 = interpret(expression_value); 25 if (rval2 != 0) 26 rval3 = inrec(iop); 27 if (rval2 == 0 || rval3 != 0) 28 break; 29 } 30 } 1Я*
548 Глава 15. Отладка 31 if (exiting) 32 break; 33 } 34 } (Номера строк приведены относительно начала этой процедуры, а не файла.) Эта функция является основой главного цикла обработки gawk. Внешний цикл (строки 12 и 33) проходит через файлы данных командной строки. Комментарий в строках 13-19 показывает оригинальный код, который читает из текущего файла каждую запись и обрабатывает ее. Возвращаемое inrec () значение 0 означает, что все в порядке, тогда как ненулевое возвращаемое значение interpret () означает, что все в порядке. Когда мы попытались пройти через этот цикл, проверяя процесс чтения записей, возникла необходимость выполнить каждый шаг отдельно. Строки 20-30 представляют переписанный код, который вызывает каждую функцию отдельно, сохраняя возвращаемые значения в локальных переменных, чтобы их можно было напечатать из отладчика. Обратите внимание, как в строке 23 этим переменным каждый раз присваиваются известные, ошибочные значения: в противном случае они могли бы сохранить свои значения от предыдущих итераций цикла. Строка 27 является тестом завершения; поскольку код изменился, превратившись в бесконечный цикл (сравните строку 22 со строкой 16), тест завершения цикла является противоположным первоначальному. В качестве отступления, мы признаемся, что нам пришлось тщательно изучить переделку, когда мы ее сделали, чтобы убедиться, что она точно соответствует первоначальному коду; она соответствовала. Теперь нам кажется, что, возможно, вот эта версия цикла была бы ближе к оригиналу: /* Возможная замена для строк 22 - 29 */ do { rval2 = rval3 = -1; /* для отладки */ rval2 = interpret(expression_yalue); if (rval2 != 0) rval3 = inrec(iop); } while (rval2 != 0 && rval3 == 0); Правда в том, что обе версии труднее воспринимать, чем оригинал, и поэтому, возможно, содержат ошибки. Однако, поскольку текущий код работает, мы решили оставить как есть. Наконец, мы обращаем внимание, что не все программисты-эксперты согласились бы здесь с нашим советом. Когда каждый компонент условия является вызовом функции, можно установить на каждую контрольную точку, использовать step для входа в каждую функцию, а затем использовать finish для ее завершения. GDB сообщит вам возвращаемое функцией значение, и с этого места вы можете использовать для продолжения cont или step. Нам нравится наш подход, поскольку результаты сохраняются в переменных, которые можно проверить (и неоднократно) после вызова функции и даже спустя несколько операторов.
15.4. Программирование для отладки 549 15.4.1.4. Используйте вспомогательные функции отладки Типичной методикой, применимой во многих случаях, является использование набора значений флагов; когда флаг установлен (т.е. равен true), имеет место определенный факт или применяется определенное условие. Обычно это осуществляется при помощи именованных констант #define и битовых операторов С. (Использование битовых флагов и операторы работы с битами мы обсуждали во врезке к разделу 8.3.1 «Стиль POSIX: statvfsO и f statvf s () ».) Например, главная структура данных gawk называется NODE. У нее большое количество полей, последнее из которых является набором значений флагов. Из файла awk.h: typedef struct exp_node { ' /* ... Куча материала опущена */' unsigned short flags; может быть освобожден */ должен быть освобожден */ не может быть освобожден */ назначен в виде строки */ текущее значение строковое */ текущее значение числовое */ назначен в виде числа */. ввод пользователя: если NUMERIC, тогда NUMBER */ размер массива максимальный */ параметр представляет имя функции; см. awkgram.y */ это является полем */ использовать локализованную версию */ } NODE; Причина для использования значений флагов заключается в том, что они значительно экономят пространство данных. Если бы структура NODE для каждого флага использовала отдельное поле char, потребовалось бы 12 байтов вместо 2, используемых unsigned short. Текущий размер NODE (на Intel x86) 32 байта. Добавление лишних 10 байтов увеличило бы ее до 42 байтов. Поскольку gawk может потенциально выделять сотни и тысячи (или даже миллионы) NODE10, сохранение незначительного размера является важным. Что это должно делать с отладкой? Разве мы не рекомендовали только что использовать для именованных констант enum? Ну, в случае объединяемых побитовыми ИЛИ значений enum не помогают, поскольку они больше не являются индивидуально распознаваемыми! Рекомендация: предусмотрите функцию для преобразования флагов в строки. Если у вас есть несколько независимых флагов, установите процедуру общего назначения. # # # # # # # # # # # # define define define define define define define define define define define define MALLOC TEMP PERM STRING STRCUR NUMCUR NUMBER MAYBE_NUM ARRAYMAXED FUNC FIELD INTLSTR 1 2 4 8 16 32 64 128 256 512 1024 2048 / / / / / / / / / / / / 0 Серьезно! Часто люди пропускают через gawk мегабайты данных. Помните, никаких произвольных ограничений! - Примеч. автора.
550 Глава 15. Отладка IЗАМЕЧАНИЕ. Необычность этих функций отладки заключается в том, что код \ приложения никогда их не вызывает. Они существуют лишь для того, чтобы их I можно было вызывать из отладчика. Такие функции всегда должны быть откомпилированы с кодом, даже без окружающих #ifdef, чтобы их можно было использо- Хвать, не предпринимая никаких дополнительных шагов. Увеличение (обычно минимальное) размера кода оправдывается экономией времени разработчика. Сначала мы покажем вам, как мы это делали первоначально. Вот (сокращенная версия) f lags2str () из ранней версии gawk C.0.6): 1 /* flags2str делает значения флагов удобочитаемыми */ 2 3 char * 4 flags2str(flagval) 5 int flagval; 6 ( ; 7 static char buffer[BUFSIZ]; 8 char *sp; 9 10 sp = buffer; 11 12 if (flagval & MALLOC) { 13 . strcpy(sp, "MALLOC"); 14 sp +=. strlen(sp); 15 } 16 if (flagval & TEMP) { 17 if (sp != buffer) 18 *sp++ = '|'; 19 strcpy(sp, "TEMP"); 20 sp += strlen(sp); 21 } 22 if (flagval & PERM) { 23 if (sp != buffer) 24 *sp++ = '|'; 25 -strcpy(sp, "PERM"); 26 sp += strlen(sp); 27 } /* ...многое то же самое, опущено для краткости... */ 82 83 return buffer; 84 } (Номера строк даны относительно начала функции.) Результатом является строка, что- то наподобие " MALLOC | PERM | NUMBER". Каждый флаг тестируется отдельно, и есл и он присутствует, действие каждый раз одно и то же: проверка того, что он не в начале
15.4. Программирование для отладки 551 буфера и что можно добавить символ ' |', скопировать строку на место и обновить указатель. Сходные функции существовали для форматирования и отображения других видов флагов в программе. Этот код является повторяющимся и склонным к ошибкам, и для gawk 3.1 мы смогли упростить и обобщить его. Вот как gawk делает это сейчас. Начиная с этого определения в awk. h: /* для целей отладки */ struct flagtab { int val; /* Целое значение флага */ const char *name; /* Строковое имя */ >; Эту структуру можно использовать для представления любого набора флагов с соответствующими строковыми значениями. Каждая отдельная группа флагов имеет соответствующую функцию, которая возвращает печатное представление флагов, которые установлены в настоящее время. Из eval. с: /* flags2str делает значения флагов удобочитаемыми */ const char * flags2str(int flagval) { static const struct flagtab values[] = { { MALLOC, "MALLOC" }, { TEMP, "TEMP" }, { PERM, "PERM" }, { STRING, "STRING" }, { STRCUR, "STRCUR" }, { NUMCUR, "NUMCUR" }, { NUMBER, "NUMBER" }, { MAYBE_NUM, "MAYBE_NUM" }, { ARRAYMAXED, "ARRAYMAXED" }, { FUNC, "FUNC" }, { FIELD, "FIELD" }', { INTLSTR, "INTLSTR" }, { 0, NULL }, >; return genflags2str(flagval, values); } flags2str () определяет массив сопоставлений флагов со строками. По соглашению, значение флага 0 означает конец массива. Код вызывает для осуществления работы genf lags2str () («общий флаг в строку»), getf lags2str () является процедурой общего назначения, которая преобразует значение флага в строку. Из eval. с: 1 /* genflags2str — общая процедура для преобразования значения флага в строковое представление */ 2 3 const char *
552 Глава 15. Отладка 4 genflags2str(int flagval, const struct flagtab *tab) 5 { 6 static char buffer[BUFSIZ]; 7 char *sp; 8 int i, space__left, space_needed; 9 10 sp = buffer; 11 space_left = BUFSIZ; 12 for (i = 0; tab [i] .name != NULL; i++) { 13 if ((flagval & tabfi] .val) != 0) { 14 /* 15 * обратите внимание на уловку, нам нужны 1 или 0, чтобы 16 * определить, нужен ли нам символ '|'. 17 */ 18 space__needed = (strlen(tab[i].name) + (sp != buffer)); 19 if (space_left < space_needed) 20 fatal(_("buffer overflow in genflags2str")); 21 22 if (sp != buffer) { 23 *sp++ = •|'; 24 space_left--; 25 } 2 6 strcpy(sp, tab[i].name); 27 /* обратите внимание на расположение! */ 28 space_left -= strlen(sp); 29 sp += strlen(sp); 30 } 31 } 32 3 3 return buffer; 34 } (Номера строк приведены относительно начала функции, а не файла.) Как и в предыдущей версии, идея заключалась в заполнении статического буфера строковыми значениями, такими, как "MALLOC | PERM] STRING|MAYBE_NUM", и возвращении адреса этого буфера. Мы вскоре обсудим причины использования статического буфера; сначала давайте исследуем код. Указатель sp отслеживает положение следующего пустого слота в буфере, тогда как space_lef t отслеживает количество оставшегося места; это уберегает нас от переполнения буфера. Основную часть функции составляет цикл, (строка 12), проходящий через массив значений флагов. Когда флаг найден (строка 13), код вычисляет, сколько места требуется строке (строка 18) и проверяет, осталось ли столько места (строки 19-20).
15.4. Программирование для отладки 553 Тест 'sp ! = buffer' для первого значения флага завершается неудачей, возвращая 0. Для последующих флагов тест дает значение 1. Это говорит нам, что между значениями должен быть вставлен разделительный символ ' | \ Добавляя результат A или 0) к длине строки, мы получаем правильное значение space_needed. Тот же тест с той же целью проводится в строке 22 для проверки строк 23 и 24, которые вставляют символ ' |'. В заключение строки 26-29 копируют значение строки, выверяют количество оставшегося места и обновляют указатель sp. Строка 33 возвращает адрес буфера, который содержит печатное представление строки. Теперь несколько слов относительно статического буфера. Обычно хорошая практика программирования не одобряет использование функций, возвращающих адреса статических буферов: множественные вызовы таких функций каждый раз переписывают буфер, вынуждая вызывающего копировать возвращенные данные. Более того, статический буфер по определению является буфером фиксированного размера. Что случилось с принципом GNU «никаких произвольных ограничений»? Для ответа на эти вопросы нужно вспомнить, что это отладочная функция. Обычный код никогда не вызывает getf lags2str (); она вызывается лишь человеком, использующим отладчик. Ни у одного вызывающего нет указателя на буфер; как разработчику, осуществляющему отладку, нам нет дела до того, что буфер каждый раз переписывается при вызове функции. На практике фиксированный размер также не является проблемой; мы знаем, что размер BUFSIZ достаточен для представления всех флагов, которые мы используем. Тем не менее, поскольку мы опытные и знаем, что вещи могут измениться, в getf lags2str () есть код, предохраняющий себя от переполнения буфера. (Переменная space_lef t и код в строках 18-20.) В качестве отступления, использование BUFSIZ спорно. Эта константа должна использоваться исключительно для буферов ввода/вывода, но часто она используется также для общих строковых буферов. Такой код лучше убрать, определив явные константы, такие, как FLAGVALSIZE, и использовав в строке 11 'sizeof (buffer)'. Вот сокращенный сеанс GDB, показывающий использование f lags2str (): $ gdb gawk /* Запустить GDB с gawk */ GNU gdb 5.3 (gdb) break do_print /* Установить контрольную точку */ Breakpoint 1 at 0x805a584: file builtin.c, line 1547. (gdb) run 'BEGIN { print "hello, world" }' /* Запустить программу */ Starting program: /home/arnold/Gnu/gawk/gawk-3.1.4/gawk 'BEGIN { print "hello, world" }• Breakpoint 1, do_print (tree=0x80955b8) at builtin.c: 1547 /* Остановка в контрольной точке */ 1547 struct redirect *rp = NULL; (gdb) print *tree /* Вывести NODE */ $1 = {sub = {nodep = {1 = {lptr = 0x8095598, paranwiame = 0x8095598 "xU\t\b",
554 Глава 15. Отладка 11 = 134629464}, г = {rptr = 0x0, pptr = 0, preg = 0x0, hd = 0x0, av = 0x0, r_ent =0}, x = {extra = 0x0, xl = 0, param_list = 0x0}, name = 0x0, number = 1, reflags = 0}, val = { fltnum = 6.6614606209589101e-316, sp = 0x0, slen = 0, sref = 1, idx = 0}, hash = {next = 0x8095598, name = 0x0, length = 0, value = 0x0, ref = 1}}, type = Node_K_print, flags =1} (gdb) print flags2str(tree->flags) /* Вывести значение флага */ $2 = 0x80918a0 "MALLOC" (gdb) next /* Продолжить */ 1553 fp = redirect_to_fp(tree->rnode, & rp) ; 1588 efwrite(t[i]->stptr, sizeof(char), t[i]->stlen, fp, "print", rp, FALSE); (gdb) print *t[i] /* Снова вывести NODE */ $4 = {sub = {nodep = {1 = {lptr = 0x8095598, param_name = 0x8095598 "xU\t\b\ 11 = 134829464}, r = {rptr = 0x0, pptr = 0, preg = 0x0, hd = 0x0, av = 0x0, r_ent =0}, x = {extra = 0x8095ad8, xl = 134830808, param_list = 0x8095ad8}, name = Oxc <Address Oxc out of bounds>, number = 1, reflags = 4294967295}, val = { fltnum = 6.6614606209589101e-316, sp = 0x8095ad8 "hello, world", slen = 12, sref = 1, idx = -1}, hash = {next = 0x8095598, name = 0x0, length = 134830808, value = Oxc, ref = 1}}, type = Node_val, flags = 29} (gdb) print flags2str(t[i]->flags) /* Вывести значение флага */ $5 = 0x80918a0 "MALLOC|PERM|STRING|STRCUR" Надеемся, вы согласитесь, что настоящий механизм общего назначения значительно более элегантный и более простой в использовании, чем первоначальный. Тщательное проектирование и использование массивов структур часто может заменить или слить воедино повторяющийся код. 15.4.1.5. По возможности избегайте объединений «Не бывает бесплатных обедов» - Lazarus Long - union С относительно эзотерическая возможность. Она помогает экономить память, сохраняя различные элементы в одном и том же физическом пространстве; как программа интерпретирует его, зависит от способа доступа: /* chl5-union.c краткая демонстрация использования union. */ #include <stdio.h> int main(void) { union i_f { int i ; float- f;
15.4. Программирование для отладки 555 > и; u.f = 12.34; /* Присвоить значение с плавающей точкой */ printf("%f also looks like %#x\n", u.f, u.i); exit@); } Вот что происходит, когда программа запускается на системе Intel x86 GNU/Linux: $ chl5-union 12.340000 also looks like 0x414570a4 Программа выводит битовый паттерн, который представляет число с плавающей точкой в виде шестнадцатеричного целого. Оба поля занимают одно и то же место в памя- . ти; разница в том, как этот участок памяти интерпретируется: u. f действует, как число ч с плавающей точкой, тогда как эти же биты в u. i действуют, как целое число. Объединения особенно полезны в компиляторах и интерпретаторах, которые часто создают древовидные структуры, представляющие структуру файла с исходным кодом (которая называется деревом грамматического разбора (parse tree)). Это моделирует то, как формально описаны языки программирования: операторы if, операторы while, операторы присваивания и так далее для всех экземпляров более общего типа «оператора». Таким образом, в компиляторе могло бы быть нечто подобное этому: struct if_stmt { ... }; /* Структура для оператора IF */ struct while_stmt { ... }; /* Структура для оператора WHILE */ struct for_stmt { ... }; /* Структура для оператора */ /* ...структуры для других типов операторов... */ typedef enum stmt_type { IF, WHILE, FOR, ... } TYPE; /* Что у нас есть в действительности */ /* Здесь содержатся тип и объединения отдельных видов операторов. */ struct statement { TYPE type; union stmt { struct if__stmt if__st; struct while_stmt while_st; struct for_stmt for_st; } u; }; Вместе с объединением удобно использовать макрос, который представляет компоненты объединения, как если бы они были полями структуры. Например: #define if_s u.if_st /* Так что можно использовать s->if_s вместо s->u.if_st */ #define while_s u.while_st /* И так далее... */ #define for_s u.for_st
556 Глава 15. Отладка На только что представленном уровне это кажется разумным и выглядит осуществимым. В действительности, однако, все сложнее, и в реальных компиляторах и интерпретаторах часто есть несколько уровней вложенных структур и объединений. Сюда относится и gawk, в котором определение NODE, значение его флагов и макросов для доступа к компонентам объединения занимают свыше 120 строк!11 Здесь достаточно определений, чтобы дать вам представление о том, что происходит: typedef struct exp__node { union { struct { union { struct exp__node *lptr; char *param_name; long 11; } 1; union { } r; union { } x; char *name; short number; unsigned long reflags; } nodep; struct { AWKNUM fltnum; char *sp; size_t slen; long sref; int idx; } val; struct { struct exp_node *next; char *name; size_t length; struct exp_node *value; long ref; } hash; #define hnext sub.hash.next Мы унаследовали эту схему. В общем, она работает, но все же есть проблемы. Целью данного раздела является передача накопленного нами в ходе работы с объединениями опыта. - Примеч. автора.
15.4. Программирование для отладки 557 #define hnarae sub.hash.name #define hlength sub.hash.length #define hvalue sub.hash.value } sub; NODETYPE type; unsigned short flags; } NODE; #define vname sub.nodep.name #define exec_count sub.nodep.reflags #define lnode sub.nodep.1.lptr #define nextp sub.nodep.1.lptr #define source_file sub.nodep.name #define source_line sub.nodep.number #define param_cnt sub.nodep.number #define param sub.nodep.1.param_name #define stptr sub.val.sp #define stlen sub.val.slen #define stref sub.val.sref #define stfmt sub.val.idx #define var_value lnode В NODE есть объединение внутри структуры внутри объединения внутри структуры! (Ой.) Поверх всего этого многочисленные «поля» макросов соответствуют одним и тем же компонентам struct /union в зависимости от того, что на самом деле хранится в NODE! (Снова ой.) Преимуществом такой сложности является то, что код С сравнительно ясный. Нечто вроде tNF_node->var__value~>slen' читать просто. У такой гибкости, которую предоставляют объединения, конечно, есть своя цена. Когда отладчик находится глубоко во внутренностях вашего кода, вы не можете использовать симпатичные макросы, которые имеются в исходном коде. Вы долэюны использовать развернутое значение12. (А для этого придется найти в заголовочном файле соответствующее определение.) Например, сравните 'NF_node->var_yalue->slen' с развернутой формой: MsrF_node->sub. nodep. 1. lptr->sub. val. slen'! Чтобы увидеть значение данных, 12 Опять-таки, GCC 3.1 или более новый и GDB 5 дают возможность непосредственного использования макросов, но только лишь если вы используете их совместно, с определенными опциями. Это было описано ранее в разделе 15.4.1.2 «По возможности избегайте макросов с выражениями». - Примеч. автора.
558 Глава 15. Отладка вы должны набрать последнее в GDB. Взгляните снова на это извлечение из приведенного ранее сеанса отладки GDB: (gdb) print *tree /* Вывести NODE */ $1 = {sub = {nodep = {1 = {lptr = 0x8095598, param_name = 0x8095598 "xU\t\b", 11 = 134829464}, r = {rptr = 0x0, pptr = 0, preg = 0x0, hd = 0x0, av = 0x0, r_ent = 0}, x = {extra = 0x0, xl = 0, param__list = 0x0}, name = 0x0, number = 1, reflags = 0}, val = { fltnum = 6.6614606209589101e-316, sp = 0x0, slen = 0, sref = 1, idx = 0}, hash = {next = 0x8095598, name = 0x0., length = 0, value = 0x0, ref = 1}}, type = Node_K_print, flags = 1} Это куча вязкой массы. Однако, GDB все же несколько упрощает ее обработку. Вы можете использовать выражения вроде '($1) . sub. val. slen', чтобы пройти через дерево и перечислить структуры данных. Есть другие причины для избегания объединений. Прежде всего, объединения не проверяются. Ничто, кроме внимания программиста, не гарантирует, что когда вы получаете доступ к одной части объединения, вы получаете доступ к той части, которая была сохранена последней. Мы видели это в chl5-union.c, в котором доступ к обоим «элементам» объединения осуществлялся одновременно. Вторая причина, связанная с первой, заключается в осторожности с перекрытиями вложенных комбинаций struct /union. Например, в предыдущей версии gawk13 был такой код: /* n->lnode перекрывает размер массива, не вызывайте unref, если это массив */ if (n->type != Node_var_array && n->type != Node_array_ref) unref(n->lnode); Первоначально if не было, был только вызов unref О, которая освобождает NODE, на которую указывает n->lnode. Однако, в этот момент gawk могла создать аварийную ситуацию. Можете себе представить, сколько времени потребовало отслеживание в отладчике того факта, что то, что рассматривалось как указатель, на самом деле было размером массива! В качестве отступления, объединения значительно менее полезны в C++. Наследование и объектно-ориентированные возможности создают при управлении структурами данных совсем другую ситуацию, которая значительно безопаснее. Рекомендация: по возможности избегайте объединений (union). Если это невозможно, тщательно проектируйте и программируйте их! 15.4.2. Отлаживаемый код времени исполнения Помимо тех вещей, которые вы добавляете к своему коду для времени компиляции, можно также добавить дополнительный код для обеспечения возможностей отладки времени исполнения. Это особенно полезно для приложений, которые устанавливаются в поле- Эта часть кода была с тех пор пересмотрена, поэтому там больше нет этих строк из примера. - Примеч. автора.
15.4. Программирование для отладки 559 вых условиях, когда в системе клиента не будет установленного исходного кода (а может быть, даже и компилятора!) В данном разделе представлены некоторые методики отладки, которые мы использовали в течение ряда лет, от простых до более сложных. Обратите внимание, что наше рассмотрение ни в коем случае не является исчерпывающим. Это область, в которой стоит иметь некоторое воображение и использовать его! 15.4.2.1. Добавляйте отладочные опции и переменные Простейшей методикой является наличие опции командной строки, делающих возможным отладку. Такая опция может быть условно откомпилированной для отладки. Однако более гибким подходом является оставить опцию в готовой версии программы. (Вы можете также решить, оставлять или не оставлять эту опцию не документированной. Здесь есть различные компромиссы: ее документирование может дать возможность вашим покупателям или клиентам больше изучить внутренности вашей системы, чего вы можете не хотеть. С другой стороны, не документирование ее кажется довольно подлым. Если вы пишете для Open Source или Free Software, лучше документировать опцию.) Если ваша программа большая, отладочная опция может принимать аргумент, указывающий, какую подсистему следует отлаживать. На основе этого аргумента можно установить различные флаговые переменные или, возможно, различные флаговые биты в одной отладочной переменной. Вот схема этой методики: struct option options[] = { { "debug", required_argument, NULL, 'D1 }, } int main(int argc, char **argv) ! { int с ; while ( (c = getopt__long (argc, argv, "...D:")) != -1) { switch (c) { case 'D': parse_debug(optarg); break; } } }
560 Глава 15. Отладка Функция parse__debug () считывает строку аргументов. Например, это может быть строка разделенных запятыми или пробелами подсистем, вроде "file,memory, ipc". Для каждого действительного имени подсистемы функция устанавливает бит в отладочной переменной: extern int debugging; void parse_debug(const char *subsystems) { char *sp; for (sp = subsystems; *sp != '\0';) { if (strncmp(sp, "file", 4) == 0) { debugging |= DEBUG_FILE; sp += 4; } else if (strncmp(sp, "memory", 6) ==0) { debugging |= DEBUG_MEM; sp += б; } else if (strncmp(sp, "ipc", 3) == 0) { debugging |= DEBUG_IPC; sp += 3; ; } while (*sp == ' ' || *sp == ',') sp++; } } * 4 В конечном счете код приложения может затем проверить флаги: if ((debugging & DEBUG_FILE) != 0) ... /* В части программы для ввода/вывода */ if ((debugging & DEBUG_MEM) . != 0) ... /* В менеджере памяти */ Использовать ли одну переменную с флаговыми битами, различные переменные или даже отладочный массив, индексируемый именованными константами (желательно из enum), зависит от вас. Ценой оставления отладочного кода в исполняемом файле изделия является увеличение размера программы. В зависимости от размещения отладочного кода он может быть также более медленным, поскольку каждый раз осуществляются проверки, которые все время оказываются ложными, пока не будет включен режим отладки. И, как упоминалось, кто-нибудь может изучить вашу программу, что может быть неприемлемым для вас. Или еще хуже, недоброжелательный пользователь может включить столько отладочных возможностей, что программа замедлится до невозможности работать с ней! (Это называется атакой отказа в обслуживании (denial of service attack).} Преимуществом, которое может быть большим, является то, что вашу уже установленную программу можно запустить с включенным режимом отладки без необходимости
15.4. Программирование для отладки 561 сначала построить, а затем загрузить специальную версию на сайт заказчика. Когда программное обеспечение установлено в удаленных местах, в которых может не быть людей и все, что вы можете сделать, это получить удаленный доступ к системе через Интернет (или, еще хуже, через медленное модемное соединение!), такая возможность может оказаться спасительным средством. Наконец, можно использовать смешанную методику: условно компилируемый отладочный код для детальной, точной отладки, а постоянно присутствующий код для более грубого вывода. v • 15.4.2.2. Используйте специальные переменные окружения Другой полезной уловкой является проверка вашим приложением специальных переменных окружения (документированных или иных). Это может быть особенно полезным для тестирования!. Вот другой пример из нашего опыта с gawk, но сначала немного основ. gawk использует функцию с названием optimal_buf size () для получения оптимального размера буфера для ввода/вывода. Для небольших файлов функция возвращает размер файла. В противном случае, если файловая система определяет размер для использования при вводе/выводе, возвращается это значение (член st_blksize структуры struct stat, см. раздел 5.4.2 «Получение информации о файле»). Если этот член недоступен, optimal_buf size () ,возвращает константу BUFSIZ из <stdio.h>. Оригинальная функция (в posix/gawkmisc . с) выглядела следующим образом:, I /* optimal_bufsize определяет оптимальный размер буфера */ 2 3 int 4 optimal_bufsize(fd/ stb) /* int optiraal_bufsize(int fd, struct stat *stb) / */ 5 int fd; 6 struct stat *stb; 7 { 8 /* инициализировать все члены нулями на случай, если ОС не использует их все. */ 9 memset(stb/ '\0', sizeof(struct stat)); 10 II /* . 12 * System V.n, n < 4, не имеет в структуре stat размера 13 * системного блока файла. Поэтому нам нужно сделать разумную 14 * догадку. Мы используем BUFSIZ, поскольку именно это имелось 15 * в виду на первом месте. 16 */ ' 17 #ifdef HAVE_ST_BLKSIZE 18 #define DEFBLKSIZE (stb->st_blksize ? stb->st_blksize : BUFSIZ) 19 #else 20 #define DEFBLKSIZE BUFSIZ 21 #endif 22
562 Глава 15. Отладка 23 if (isatty(fd)) 24 return BUFSIZ; 25 if (fstat(fd, stb) == -1) 26 fatal("can't stat fd %d (%s)", fd, strerror(errno)); 27 if (lseek(fd, (off_tH, 0) == -1) /* не обычный файл */ 2 8 return DEFBLKSIZE; 29 if (stb->st_size > 0 && stb->st_size < DEFBLKSIZE) /* маленький файл */ 30 return stb->st_size; 31 return DEFBLKSIZE; 32 } Константа DEFBLKSIZE является «размером блока по умолчанию»; то есть значением из struct stat или BUFSIZ. Для терминалов (строка 23) или файлов, которые не являются обычными файлами (lseek () завершается неудачей, строка 27) возвращаемое значение также равно BUFSIZ. Для небольших обычных файлов используется размер файла. Во всех других случаях возвращается DEFBLKSIZE. Знание «оптимального» размера буфера особенно полезно в файловых системах, в которых размер блока больше BUFSIZ. У нас была проблема, когда один из наших контрольных примеров отлично работал на нашей рабочей системе GNU/Linux и на любой другой системе Unix, к которой у нас был доступ. Однако, этот тест последовательно терпел неудачу на других определенных системах. В течение длительного времени мы не могли получить непосредственный доступ к терпящей неудачу системе, чтобы запустить GDB. В конце концов, мы смогли, однако, ухитриться воспроизвести проблему. Она оказалась связана с размером буфера, который gawk использовал для чтения файлов данных: на терпящих неудачи системах размер буфера был больше, чем на нашей системе разработки. Нам был нужен способ воспроизведения проблемы на своей машине разработки: система с неудачей находилась в стороне за девять часовых поясов, а интерактивный запуск GDB через Атлантический океан мучителен. Мы воспроизвели проблему, заставив optimal_buf size () проверять значение специальной переменной окружения AWKBUFSIZE. Когда ее значение равно "exact", optimal_buf size () всегда возвращает размер файла, каким бы он ни был. Если значением AWKBUFSIZE является какое- нибудь целое число, функция возвращает это число. В противном случае, функция возвращается к прежнему алгоритму. Это дает нам возможность запускать тесты, не требуя постоянной перекомпиляции gawk. Например, $ AWKBUFSIZE-42 make check Это запускает тестовый набор gawk с использованием размера буфера в 42 байта. (Тестовый набор проходит.) Вот модифицированная версия optimal_buf size (): 1 /* optimal_bufsize определение оптимального размера'буфера */ 2 3 /* '4 * В целях отладки усовершенствуйте это следующим образом: 5 *
15.4. Программирование для отладки 563 6 * Всегда используйте stat для файла, буфер stat используется кодом 7 * более высокого уровня. 8 * if (AWKBUFSIZE == "exact") 9 * return the file size 10 * else if (AWKBUFSIZE == число) 11 * всегда возвращать это число 12 * else 13 * if размер < default_blocksize 14 * return размер 15 * else 16 * return default_blocksize 17 * end if 18 * endif 19 * 20 * Приходится повозиться, чтобы иметь дело с AWKBUFSIZE лишь 21 * однажды, при первом вызове этой процедуры, а не при каждом 22 * ее вызове. Производительность, знаете ли. 23 */ 24 25 size_t 26 optimal_bufsize(fd, stb) 27 int fd; . 28 struct stat *stb; 29 { 3 0 char *val; 31 static size_t env_val = 0; 32 static short first = TRUE; 33 static short exact = FALSE; 34 3 5 /* обнулить все члены, на случай, если ОС их не использует. */ 36 memset(stb, ' \0', sizeof(struct stat)); 37 3 8 /* всегда использовать stat на случай, если stb используется кодом более вьсокого уровня */ 39 if (fstat(fd, stb) == -1) 40 fatal("can*t stat fd %d (%s)", fd, strerror(errno)); 41 42 if (first) { 43 first = FALSE; 44 45 if ((val = getenv("AWKBUFSIZE")) != NULL) { 46 if (strciup(val, "exact") == 0) 47 exact = TRUE; 48 else if (ISDIGIT(*val)) {
564 Глава 15. Отладка 49 for (; *val 8c8c ISDIGIT(*val) ; val++) 50 env_val = (env_val * 10) + *val - ' 0.' ; 51 52 return env_val; 53 } 54 } 55 } else if (! exact && env_val > 0) 5 6 return env_val; 57 /* else 58 обрабатывать дальше */ 59 60 /* 61 * System V.n, n < 4, не. имеет в структуре stat размера системного 62 * блока файла. Поэтому нам нужно осуществить разумную догадку. 63 . * Мы используем BUFSIZ из stdio, поскольку именно это имелось 64 *. в виду прежде всего. 65 */ 66 #ifdef HAVE__ST_BLKSIZE 67 #define DEFBLKSIZE (stb->st_blksize > 0 ? stb->st_blksize : BUFSIZ) 68 #else 69 #define DEFBLKSIZE BUFSIZ 70 #endif 71 72 if (S_ISREG(stb->st_mode) /* обычный файл */ 73 && 0 < stb->st_size /* ненулевой размер */ 74 ScSc (stb->st_size < DEFBLKSIZE /* маленький файл */ 7 5 || exact)) /* или отладка */ 7 6 return stb->st_size; /* использовать размер файла*/ 77 7 8 return DEFBLKSIZE; 79 } Комментарий в строках 3-23 объясняет алгоритм. Поскольку поиск переменных окружения может быть затратным и его нужно осуществить лишь однажды, функция использует для сбора соответствующих сведений в первый раз несколько статических переменных. Строки 42-54 выполняются лишь при первом вызове функции. Строка 43 обеспечивает это условие, устанавливая в first значение false. Строки 45-54 обрабатывают переменную окружения, разыскивая либо строку "exact", либо число. В последнем случае оно преобразуется из строкового значения в десятичное, сохраняясь в env_yal. (Возможно, нам следовало бы использовать здесь strtoul (); в свое время это не пришло нам на ум.) Строка 55 выполняется каждый раз, кроме первого. Если было представлено числовое значение, условие будет истинным, и возвращается это значение (строка 56). В противном случае, исполнение переходит к оставшейся части функции.
15.4. Программирование для отладки 565 Строки 60-70 определяют DEFBLKSIZE; эта часть не изменилась. Наконец, строки 72-76 возвращают размер файла, если это приемлемо. Если нет (строка 78), возвращается DEGBLKSIZE. Мы действительно устранили проблему14, но между тем оставили на месте новую версию optimal_buf size (), чтобы можно было убедиться, что проблема не возникнет вновь. Незначительное увеличение размера кода и его сложности более чем компенсируется возросшей гибкостью, которая есть теперь у нас для тестирования. Более того, поскольку это код изделия, пользователь в полевых условиях может с легкостью использовать эту особенность для тестирования, чтобы определить, не появилась ли сходная проблема. (До сих пор нам не приходилось просить проделать этот тест, но приятно осознавать, что мы могли бы это сделать, если бы пришлось.) 15.4.2.3. Добавьте коджурналирования Часто бывает так, что программа вашего приложения работает на системе, на которой вы не можете использовать отладчик (как в случае сайта клиента). В таком случае вашей целью является возможность проверки внутреннего состояния программы, но извне. Единственным способом сделать это является заставить саму программу предоставить для вас эту информацию. Для этого существует множество способов: Всегда записывайте сведения в специфический файл. Это простейший способ: программа всегда записывает регистрационную информацию. Затем вы можете при возможности просмотреть файл. Недостаток в том, что в какой-то момент регистрационный файл займет все дисковое пространство. Следовательно, у вас должны быть несколько файлов журналов, причем программа периодически должна переключаться между ними. Брайн Керниган рекомендует называть файлы журнала по дням недели: myapp. log. sun, myapp. log.mon и т. д. Преимуществом здесь является то, что вам не придется вручную удалять старые файлы; вы бесплатно получаете недельную стоимость файлов журналов. • Записывайте в файл журнала лишь тогда, когда он уже существует. При запуске ваша программа записывает сведения в файл журнала, если он существует. В противном случае записи не происходит. Чтобы включить журналирование, сначала создайте пустой файл журнала. • Используйте для сообщений фиксированный формат, который можно легко анализировать с помощью языков сценариев, таких, как awk или Perl, для создания сводок и отчетов. • В качестве альтернативы можно создать какую-нибудь разновидность XML, который является самоописывающимся и допускающим преобразование в другие форматы. (Мы не являемся большими поклонниками XML, но вас это не должно останавливать.) • Для журналирования используйте syslogO; конечное расположение сообщений журналирования может контролироваться системным администратором, (syslog () является довольно продвинутым интерфейсом; см. справочную страницу syslogC).) 14 Переписав код управления буфером! - Примеч. автора.
566 Глава 15. Отладка Выбор того, как регистрировать сведения, является, конечно, легкой частью. Трудной частью является выбор того, что регистрировать. Как и в остальных случаях разработки программ, стоит подумать, прежде чем программировать. Записывайте сведения о критических переменных. Проверьте их значения, чтобы убедиться, что они в приемлемом диапазоне или в других отношениях ведут себя, как ожидается. Записывайте исключительные условия; если появляется что-то, что не должно было, зарегистрируйте это и при возможности продолжайте исполнение. Ключом является регистрация лишь тех сведений, которые вам нужны для отслеживания проблем, не больше и не меньше. 15.4.2.4. Файлы отладки времени исполнения В предыдущей жизни мы работали для начинающей компании с двоичными исполняемыми файлами продукта, установленными на сайтах клиентов. Подключить отладчик к запущенной копии программы или запустить ее из отладчика на системе клиента было невозможно. Главный компонент продукта запускался не непосредственно из командной строки, а опосредованно, через сценарии оболочки, которые проделывали значительную первоначальную настройку. Чтобы заставить программу выдавать при запуске журналируемые сведения, мы пришли к идее специальных отладочных файлов. Когда файл с определенным именем находился в определенном каталоге, программа выдавала бы информационные сообщения в файл журнала, который мы могли бы затем загрузить и проанализировать. Такой код выглядит следующим образом: struct stat sbuf; extern int do_logging; /*.инициализировано нулями */ if (stat("/path/to/magic/.file", &sbuf) ==0)' do_logging = TRUE; if (do_logging) { /* здесь код журналирования: открытие файла, запись, закрытие и * т . д. * / } Вызов stat () происходил для каждого задания, которое выполняла программа. Таким образом, мы могли динамически включать и отключать журналирование без необходимости останавливать и повторно запускать приложение! Как и в случае с отладочными опциями и переменными, в этом предмете имеется множество вариаций: различные файлы, которые запускают журналирование информации о различных подсистемах, директивы отладки, добавляемые в сам отладочный файл и т. д. Как и со всеми возможностями, следует планировать схему того, что вам будет нужно, а затем чисто ее реализовывать, вместо того, чтобы набросать какой-
15.4. Программирование для отладки 567 нибудь быстрый и грязный код в 3 часа пополудни (к сожалению, довольно типичная возможность в начинающих компаниях). ЛЗАМЕЧАНИЕ. Не все то золото, что блестит. Специальные отладочные файлы Iявляются лишь одним примером методик, известных как лазейки (back doors) - один I или более способов выполнения разработчиками недокументированных вещей с программой, обычно с бесчестными намерениями. В нашем примере лазейка была I исключительно доброкачественной. Но беспринципный разработчик легко мог бы {устроить создание и загрузку скрытой копии списка клиентов, картотеки персона- \ла или других важных данных. По одной этой причине вы должны серьезно подумать, применима ли эта методика в вашем прилоэюении. 15.4.2.5. Добавьте специальные ловушки для контрольных точек Часто проблема может быть воспроизводимой, но лишь после того, как программа сначала обработает многие мегабайты вводимых данных. Или, хотя вы и знаете, какая функция вызывает сбой, он возникает лишь после вызова этой функции сотни или даже тысячи раз. Это большая проблема, когда вы работаете в отладчике. Если вы установите контрольную точку на вызывающую сбой процедуру, вам придется набирать команду continue и нажимать ENTER сотни или тысячи раз, чтобы привести программу в состояние перед сбоем. Это по меньшей мере утомительно и способствует появлению ошибок! Это может оказаться даже таким трудным, что вы захотите отказаться от этого, даже не начав. Решение заключается в добавлении специальных отладочных функций «ловушек» («hook»), которые ваша программа может вызвать при приближении к интересующему вас состоянию. Например, предположим, что вы знаете, что функция check__salary () вызывает сбой, но лишь когда она* вызвана 1427 раз. (Мы не смеемся над вами; в свое время нам пришлось наблюдать довольно странные вещи.) Чтобы перехватить check_salary () до того, как она завершится неудачей, создайте специальную фиктивную функцию, которая ничего не делает и просто возвращается, затем сделайте так, чтобы check_salary () вызывала ее как раз перед 1427-м своим вызовом: /* debug_dummy отладочная функция-ловушка */ void debug_dummy(void) { return; } struct salary *check_salary(void) { /* ...здесь описания настоящих переменных... */ static int count = 0; /* для отладки */ if (++count == 1426) debug_dummy () ; . /*' ...оставшаяся часть кода... */ }
568 Глава 15. Отладка Теперь из GDB установите контрольную точку в debug_dummy (), а затем запустите программу обычным способом: (gdb) break debug_dummy /* Установить контрольную точку для фиктивной функции */ Breakpoint 1 at 0x8055885: file whizprog.c, line 3137. (gdb) run /* Запуск программы */ По достижении контрольной точки для debug_dummy () вы можете установить вторую контрольную точку для check_salary () и продолжить исполнение: (gdb) run /* Запуск программы */ ^ Starting program: /home/arnold/whizprog Breakpoint 1, debug_dummy () at whizprog.c, line 3137 3137 void debug_dummy(void) { return; } /* Достижение контрольной точки */ (gdb) break check_salary /* Установить контрольную точку для интересующей функции */ Breakpoint 2 at 0x8057913: file whizprog.c, line 3140. (gdb) cont По достижении второй контрольной точки программа готова завершиться неудачей, и вы можете пошагово ее пройти, делая все необходимое для отслеживания проблемы. Вместо использования фиксированной константы (Ч+count == 142 6') можно использовать глобальную переменную, которая устанавливается отладчиком в любое нужное вам значение. Это дает возможность избежать перекомпилирования программы. Для gawk мы пошли на один шаг дальше и внесли возможность отладочной ловушки в язык, так что функция ловушки могла быть вызвана из программы awk. При компилировании для отладки доступна специальная ничего не делающая функция stopme (). Эта функция, в свою очередь, вызывает функцию С с тем же названием. Это позволяет нам поместить вызовы stopmeO в завершающуюся неудачей программу awk непосредственно перед сбойным участком. Например, если gawk выдает ошибочные результаты для программы awk в 1200-й вводимой записи, мы можем добавить в программу awk строку, подобную этой: NR == 1198 { stopmeO } # Остановиться для отладки, когда число записей == 1198 /* ...оставшаяся часть программы как ранее... */ Затем из GDB мы можем установить контрольную точку на функции С stopme () и запустить программу awk. Когда контрольная точка срабатывает, мы можем затем установить контрольные точки на другие части gawk, где, как мы ожидаем, находится действительная проблема. Методика функции-ловушки полезна сама по себе. Однако, возможность переместить ее на уровень приложения умножает ее полезность, и она сохранила нам бесчисленное число часов отладки при отслеживании непонятных проблем. 15.5. Отладочные инструменты Помимо GDB и различных ловушек в исходном коде, которые вы используете для общей отладки, имеется ряд полезных пакетов, которые могут помочь обнаружить определенные разновидности проблем. Поскольку управление динамической памятью является в крупномасштабных программах такой трудной задачей, многие инструменты фокусируются на этой области, часто действуя в качестве замещающих malloc () и free () элементов.
15.5. Отладочные инструменты 569 Имеются коммерческие инструменты, которые делают множество (или все) из тех вещей, что и описываемые нами программы, но не все они доступны для GNU/Linux, а многие довольно дороги. Все пакеты, обсуждающиеся в данном разделе, являются свободно доступными. 15.5.1. Библиотека dbug - усовершенствованный print ? () Первым пакетом, который мы исследуем, является библиотека dbug. Она основана на идее условно компилируемого отладочного кода, которую мы представили ранее в данной главе, но идет намного дальше, предоставляя сравнительно сложную трассировку времени исполнения и условный вывод отладки. Она реализует многие из описанных нами советов, избавляя вас от хлопот по собственной их реализации. Библиотека dbug, написанная Фредом Фишем (Fred Fish) в начале 1980-х, была с тех пор несколько усовершенствована. Теперь она явным образом является общим достоянием, поэтому ее можно использовать без всяких проблем как в свободном, так и частном программном обеспечении. Она доступна через архив FTP Фреда Фиша15 как в виде сжатого файла tar, так и в виде архива ZIP. Документация хорошо резюмирует dbug: dbug является примером внутреннего отладчика. Поскольку она требует внутренней поддержки программы и ее использование не зависит от каких бы то ни было особых возможностей среды исполнения, она всегда доступна и будет выполняться в любом , окружении, в котором будет выполняться сама программа. Вдобавок, поскольку это законченный пакет с особым интерфейсом пользователя, все программы, которые ее используют, будут иметь сходные возможности отладки. Это резко контрастирует с другими формами внутренней поддержки, где у каждого разработчика своя собственная, обычно менее квалифицированная, форма внутреннего отладчика... Пакет dbug лишь незначительно снижает скорость выполнения программ, обычно значительно менее 10 %, и немного увеличивает их размеры, обычно от 10 до 20 %. Определив особый идентификатор препроцессора С, можно снизить оба этих показателя до нуля без необходимости изменений в исходном коде. Следующий список является кратким изложением возможностей пакета dbug. Каждую возможность можно отдельно включать или отключать во время запуска программы, указав соответствующие аргументы командной строки. • Трассировка исполнения, отображающая уровень потока управления полу- графическидо способом с использованием отступов, обозначающих глубину вложения. • Вывод.значений всех или любого набора ключевых внутренних переменных. • Ограничение действий определенным набором указанных функций. • Ограничение трассировки функций указанной глубиной вложения. • Пометку каждой выводимой строки названием исходного файла и номером строки. f tp: / / ftp. ninemoons . com/pub/dbug/ - Примеч. автора.
570 Глава 15. Отладка • Пометку каждой выводимой строки названием текущего процесса. • Сохранение в стеке или восстановление состояния отладки для обеспечения исполнения со встроенными значениями по умолчанию для отладки. • Перенаправление потока вывода отладки в стандартный вывод (stdout) или указанный файл. По умолчанию поток вывода направляется в стандартную ошибку (stderr). Механизм перенаправления полностью независим от обычного перенаправления командной строки, чтобы избежать конфликтов вывода. Пакет dbug требует от вас использования определенного порядка при написании своего кода. В частности, нужно использовать его макросы при возвращении из функции или вызове set jmp () и longjmp (). Нужно добавлять один вызов макроса в качестве первого исполняемого оператора каждой функции и вызвать несколько дополнительных макросов из main (). Наконец, нужно добавить отладочную опцию командной строки: по соглашению, это -#, которая редко используется в качестве действительной опции, если вообще используется. В обмен на дополнительную работу вы получаете все только что очерченные преимущества. Давайте взглянем на пример в руководстве: 1 #include <stdio.h> 2 #include "dbug.h" 3 4 int 5 main (argc, argv) 6 int argc; 7 char *argv[]; 8 { 9 register int result, ix; 10 extern int factorial (), atoi (); 11 12 DBUG_ENTER ("main"); 13 DBUG_PROCESS (argv[0]); 14 DBUG_PUSH_ENV ("DBUG"); . , 15 for (ix = 1; ix < argc && argv[ix][0] == '-'; ix++) { 16 switch (argv[ix][1]) { 17 case '#': 18 DBUG_PUSH (&(argv[ix] [2])); 19 break; 20 } 21 } 22 for (; ix < argc; ix++) { 23 DBUG_PRINT ("args", ("argv[%d] = %s", ix, argv[ix])) ; 24 result = factorial (atoi (argv[ix])); 25 printf ("%d\n", result); 26 fflush (stdout); 27 } 2 8 DBUG_RETURN @); 29 }
15.5. Отладочные инструменты 571 Эта программа иллюстрирует большинство важных моментов» Макрос DBUG_ENTER () (строка 12) должен быть вызван после объявлений переменных и перед любым другим кодом. (Это потому, что он сам объявляет несколько частных переменных16.) Макрос DBUG_PROCESS () (строка 13) устанавливает имя программы, главным образом, для использования в выводимых библиотекой сообщениях. Этот макрос должен вызываться лишь однажды, из main (). Макрос DBUGa_PUSH_ENV () (строка 14) заставляет библиотеку проверить указанную переменную окружения (в данном случае DBUG) на предмет управляющей строки. (Управляющие строки dbug вскоре будут рассмотрены.) Библиотека может, сохранив свое текущее состояние и использовав новое, создавать стек сохраненных состояний. Таким образом, этот макрос помещает в стек сохраненных состояний полученное от данной переменной окружения состояние. В данном примере использован случай, когда макрос создает первоначальное состояние. Если такой переменной окружения нет, ничего не происходит. (В качестве отступления, DBUG является довольно общей переменной; возможно, GAWK_DBUG было бы лучше [для gawk].) Макрос DBUG_PUSH (строка 18) передает значение управляющей строки, полученной из опции командной строки -#. (Новый код должен использовать getopt () или getopt_long () вместо ручного анализа аргументов.) Таким образом обычно включается режим отладки, но использование переменной окружения предоставляет также дополнительную гибкость. Макрос DBUG_PRINT () (строка 23) осуществляет вывод. Второй аргумент использует методику, которую мы описали ранее (см. раздел 15.4.1.1 «Используйте отладочные макросы»), по включению в скобки всего списка аргументов print f (), делая его простым аргументом, насколько это касается препроцессора С. Обратите внимание, что завершающий символ конца строки в форматирующей строке не указывается; библиотека dbug вставляет его за вас. При печати dbug по умолчанию выводит все операторы DBUG_PRINT (). Первый аргумент является строкой, которая может использоваться для ограничения вывода лишь теми макросами DBUG_PRINT (), которые используют эту строку. Наконец, макрос DBUG_RETURN () (строка 28) используется вместо обычного оператора return для возврата значения. Для использования с функциями void имеется соответствующий макрос DBUG_VOID_RETURN. Оставшаяся часть программы заполнена функцией factorial (): 1 #include <stdio.h> 2 #include "dbug.h" 3 4 int factorial (value) 5 register int value; 16 В С99, который допускает смешивание объявлений переменных с исполняема кодом, это составляет меньшую проблему, но помните, что этот пакет был разработан для K&R С. - Примеч. автора.
572 Глава 15. Отладка 6 { 7 DBUG_ENTER ("factorial"); 8 DBUG_PRINT ("find", ("find %d factorial", value)); 9 if (value > 1) { 10 value *= factorial (value - 1); 11 } 12 DBUG_PRINT ("result", ("result is %d", value)); 13 DBUG_RETURN (value); 14 } Когда программа откомпилирована и скомпонована вместе с библиотекой dbug, ее можно запустить обычным способом. По умолчанию, программа не создает вывод отладки. Но со включенной отладкой доступны различные виды вывода: $ factorial 12 3 /* Обычный запуск, без отладки */ 1 2 б $ factorial -#t 12 3/* Вьвесги трассироЕку вьвсвов функций, офатиге внимание на влонежость */ | >factorial | <factorial 1 /* Обычный вывод в stdout */ | >factorial | | >factorial | | <factorial /* Вывод отладки в stderr */ | <factorial 2 | >factorial | | >factorial , | | | >factorial | | | <factorial | | <factorial | <factorial б <?func? $ factorial -#d 12/* Показать отладочные сообщения DBUG__PRINT() */ ?func?: args: argv[2] =1 factorial: find: find 1 factorial factorial: result: result is 1 1 ?func?: args: argv[3] = 2 factorial: find: find 2 factorial factorial: find: find 1 factorial factorial: result: result is 1 factorial: result: result is 2 2
15.5. Отладочные инструменты 573 Опция -# управляет библиотекой dbug. Она «особая» в том смысле, что DBUG_PUSH() будет принимать всю строку, игнорируя ведущие символы '-#', хотя вы могли бы использовать при желании другую опцию, передав DBUG_PUSH () лишь строку аргументов опций (если вы используете getopt (), это optarg). Управляющая строка состоит из набора опций и аргументов. Каждая группа опций и аргументов отделяется от других символом двоеточия. Каждая опция представлена одной буквой, а аргументы этой опции отделяются от нее запятыми. Например: $ myprog -id/inem/ipc: f/check_salary/check_start_date -f infile -o outfile Опция d включает вывод DBUG_PRINT (), но лишь если первая строка аргумента является "mem" или "ipc". (Если аргументов нет, выводятся все сообщения DBUG_PRINT ().) Сходным образом опция f ограничивает трассировку вызовов функций лишь указанными функциями: check_salary () и check__start_date (). Следующий список опций и аргументов воспроизведен из руководства библиотеки dbug. Квадратные скобки заключают необязательные аргументы. Мы включаем здесь лишь те, которые находим полезными; полный список см. в документации. d[,ключевые слова] Разрешает вывод от макросов с указанными ключевыми словами. Пустой список ключевых слов предполагает, что выбраны все ключевые слова. F Помечает каждую строку вывода отладки именем исходного файла, содержащего макрос, осуществляющий вывод. i Идентифицирует процесс, выводящий каждую отладочную или трассировочную строку номером ID для этого процесса. L Помечает каждую строку вывода отладчика номером строки исходного файла, в котором находится осуществляющий вывод макрос. о[#файл] Перенаправляет поток вывода отладчика в указанный файл. Потоком вывода по умолчанию является stderr. Пустой список аргументов перенаправляет вывод в stdout. t[,N] Включает трассировку потока управления функций. Максимальная глубина вложения определяется N, по умолчанию используется 200. Для завершения нашего обсуждения вот остальные макросы, определенные библиотекой dbug. DBUG_EXECUTE(строка, код) Этот макрос похож на DBUG_PRINT (): первый аргумент является строкой, выбранной с помощью опции d, а второй - код для исполнения: DBUG_EXECUTE("abort", abort());
574 Глава 15. Отладка DBUG_FILE Это значение типа FILE * для использования с процедурами <stdio.h>. Оно . позволяет осуществлять собственный вывод в поток файла отладки. DBUG_LONGJMP(jmp_buf env, int val) Этот макрос заключает в оболочку вызов longjmp (), принимая те же самые аргументы, так что библиотека dbug будет знать, когда вы сделали нелокальный переход. DBUG_POP() Этот макрос выталкивает из стека один уровень сохраненного состояния отладки, созданный макросом DBUG_PUSH (). Он довольно эзотерический; вы скорее всего не. будете его использовать. DBUG_SETJMP( jmp_buf env) Этот макрос заключает в оболочку вызов setjmpO, принимая те же самые аргументы. Он позволяет библиотеке dbug обрабатывать нелокальные переходы. В другом воплощении, в первой начинающей компании, для которой мы работали , мы использовали в своем продукте библиотеку dbug. Она была неоценимой при разработке, а опустив -DDBUG в конечной сборке, мы смогли построить готовую версию без других изменений исходного кода. Чтобы извлечь максимальную выгоду от библиотеки dbug, нужно использовать ее последовательно, по всей программе. Это проще, если вы используете ее с начала проекта, но в качестве эксперимента мы обнаружили, что с помощью простого сценария awk мы смогли включить библиотеку в программу с 30 000 строк кода за несколько часов работы. Если вы можете позволить себе накладные расходы, лучше всего оставить ее в конечной сборке вашей программы, чтобы можно было ее отлаживать без необходимости предварительной перекомпиляции. Мы нашли, что библиотека dbug является удачным дополнением к внешним отладчикам, таким, как GDB; она обеспечивает организованный и последовательный способ применения поддержки к коду С. Она также довольно элегантно сочетает многие из методик, которые мы ранее в данной главе очертили отдельно. Особенно полезна особенность динамической трассировки вызовов функций, и она доказывает свою бесценность в качестве помощи в изучении поведения программы, если вы незнакомы с ней. 15.5.2. Отладчики выделения памяти Игнорируя такие проблемы, как плохой дизайн программы, для любого крупномасштабного практического приложения единственной сложной задачей программиста на С является управление динамической памятью (посредством malloc (), realloc () и free ()). Хотя нам следовало бы усвоить свой урок после первой компании, мы перешли ко второй. С тех пор, как мы это выяснили, мы обычно избегаем начинающие компании. Ваша выгода, конечно, может меняться. - Примеч. автора.
15.5. Отладочные инструменты 575 Этот факт подкреплен большим количеством инструментов, доступных для отладки динамической памяти. Имеется значительное перекрывание того, что предлагают данные утилиты. Например: Обнаружение утечек памяти; память, которая выделяется, а затем становится недоступной. • Обнаружение не освобождаемой памяти: память, которая выделяется, но никогда не освобождается. Не освобождаемая память не всегда является ошибкой, но определение таких случаев дает вам возможность проверить, что с ними все в порядке. Обнаружение неправильных освобождений: память, которая освобождается дважды, или функции free () передаются указатели, которые не были получены с помощью malloc (). • Обнаружение использования уже освобожденной памяти: память, которая освобождена, используется через висячий указатель. Обнаружение выхода за границы выделенной памяти: получение доступа или сохранение в память за пределами выделенной границы. Предупреждение об использовании неинициализированной памяти. (Многие компиляторы могут выдавать такие предупреждения.) • Динамическая трассировка функций: когда появляется ошибочный доступ к памяти, вы получаете трассировку от того места, где память используется, до того места, где она была выделена. • Управление инструментами посредством использования переменных окружения. • Файлы журналов для необработанной отладочной информации, которая может быть обработана позже для создания полезных отчетов. Некоторые утилиты просто записывают эти события. Другие организуют жуткое завершение программы приложения (посредством SIGSEGV), чтобы на код-нарушитель можно было точно указать из отладчика. Вдобавок, большинство спроектированы для работы вместе с GDB. Некоторые инструменты требуют изменения исходного кода, такого, как вызов специальных функций или использование особого заголовочного файла, дополнительных #def ine и статической библиотеки. Другие работают посредством использования специального механизма библиотек общего пользования Linux/Unix для прозрачной установки себя в качестве заместителя стандартных библиотечных версий malloc () и free (). В данном разделе мы рассмотрим три отладчика динамической памяти, а затем предоставим ссылки на несколько других. W.5.2.1. GNU/Linuxm trace Системы GNU/Linux, использующие GLIBC, предоставляют две функции для включения и отключения трассировки памяти во время исполнения: #include <mcheck.h> /* GLIBC */ void mtrace(void); void шипtrace(void); Когда вызывается mtrace (), библиотека проверяет переменную окружения MALLOC_TRACE. Ожидается, что она указывает на записываемый файл (существующий или нет). Библиотека открывает файл и начинает записывать сведения о выделениях и ос-
576 Глава 15. Отладка вобождениях памяти. (Если файл не может быть открыт, запись не производится. Файл урезается каждый раз при запуске программы.) Когда вызывается muntrace (), библиотека закрывает файл и больше не регистрирует выделения и освобождения. Использование отдельных функций дает возможность проводить трассировку памяти для определенных частей программы; необязательно отслеживать все. (Мы нашли наиболее полезным включить журналирование в начале программы и все, но эта схема предоставляет гибкость, которую хорошо иметь.) Когда приложение завершается, вы используете программу mtrace для анализа файла журнала. (Файл журнала в формате ASCII, но информацию нельзя использовать непосредственно.) Например, gawk включает трассировку, если определена TIDYMEM: $ export Т1ШМЕ№1 MALIiOC_TRAaE^rntrace.out /* Экспортировать переменные овружения */ $ ./gawk * BEGIN { print "hello, world" }* /* Запустить программу */ hello, world $ mtrace ./gawk mtrace.out /* Создать отчет */ Memory not freed: Address Size Caller 0x08085858 0x20 at /home/arnold/Gnu/gawk/gawk-3.1.3/main.с:1102 0x08085880 0xc80 at /home/arnold/Gnu/gawk/gawk-3.1.3/node.с:398 0x08086508 0x2 at /home/arnold/Gnu/gawk/gawk-3.1.3/node.с:337 0x08086518 0x6 at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:337 0x08086528 0x10 at '/home/arnold/Gnu/gawk/gawk-3.1.3/eval.c:2082 0x08086550 0x3 at /home/arnold/'Gnu/gawk/gawk-3 .1.3/node . с : 337 0x08086560 0x3 at /home/arnold/Gnu/gawk/gawk-3.1.3/node.с:337 0x080865e0 0x4 at /home/arnold/Gnu/gawk/gawk-3.1.3/field.с:76 0x08086670 0x78 at /home/arnold/Gnu/gawk/gawk-3.1.3/awkgram.у:1369 0x08086700 Oxe at /home/arnold/Gnu/gawk/gawk-3.1.3/node.с:337 0x08086718 Oxlf at /home/arnold/Gnu/gawk/gawk-3.1.3/awkgram.у:1259 Вывод представляет собой список мест, в которых gawk выделяет память, которая в дальнейшем не освобождается. Обратите внимание, что постоянное подвешивание к динамической памяти является замечательным, если это сделано намеренно. Все показанные здесь случаи являются выделениями такого рода. 15.5.2.2. Electric Fence В разделе 3.1 «Адресное пространство Linux/Unix» мы описали, как динамическая память выделяется из кучи, которая может расти и сокращаться (с помощью вызовов brk () или sbrk () /описанных в разделе 3.2.3 «Системные вызовы: brk () и sbrk () »). Ну, картина, которую мы там представили, является упрощением действительности. Более развитые системные вызовы (не рассматриваемые в данной книге) позволяют добавлять в адресное пространство процесса дополнительные, необязательно смежные сегменты памяти. Многие отладчики malloc () работают с использованием этих систем-
15.5. Отладочные инструменты 577 ных вызовов для добавления новых областей адресного пространства при каждом выделении. Преимуществом этой схемы является то, что операционная система и аппаратное обеспечение защиты памяти компьютера взаимодействуют для обеспечения недействительности доступа к памяти за пределами этих изолированных сегментов, генерируя сигнал SIGSEGV. Эта схема изображена на рис. 15.1. Стек программы Старшие адреса СЕГМЕНТ СТЕКА Дыра | Стек растет вниз —• Сюда нельзя прикасаться! Выделенная malloc f память I дыра ¦ Выделенная malloc ::: память % Дыра : t Куча BSS Data Исполняемый код (общий) ¦ Сюда нельзя прикасаться! ¦ Сюда нельзя прикасаться! Куча растет вверх СЕГМЕНТ ДАННЫХ $ Младшие адреса СЕГМЕНТ ТЕКСТА (КОДА) Рис. 15.1. Адресное пространство Linux/Unix, включая специальные области Первым пакетом отладки, реализовавшим эту схему, был Electric Fence. Electric Fence является вставляемым заместителем для malloc () и др. Он работает на многих системах Unix и GNU/Linux; он доступен с FTP архива его авторов18. Он поставляется также ftp: //ftp.perens . com/pub/ ElectricFence-Я/лшеч. автора. 19-159
578 Глава 15. Отладка со многими дистрибутивами GNU/Linux, хотя, возможно, вам придется выбрать его явным образом при установке системы. После компоновки программы с Electric Fence любой доступ за пределами выделенной памяти генерирует SIGSEGV. Electric Fence также перехватывает попытки использования уже освобожденной памяти. Вот простая программа, которая иллюстрирует обе проблемы: 1 /* chl5-badmeml. с плохо обращается с памятью */ 2 3x#include <stdio.h> 4 #include <stdlib.h> 5 6 int main(int argc, char **argv) 7 { . 8 char *p; 9 int i; lO- ll p = mallocC0) ; 12 13 strcpy(p, "not 30 bytes"); 14 printfC'p = <%s>\n'\ p) ; 15 16 if (argc == 2) { 17 if (strcmp(argv[l], "-b") == 0) 18 p[42] = 'a'; /* коснуться за пределами границы */ 19 else if (strcmp(argv[l], "-f") == 0) { 20 free(p); /* освободить память, затем использовать ее */ 21 р [ 0 ] = ' b ' ; 22 } 23 } 24 2 5 /* освобождение (р); */ 26 27 return 0; 28 } Эта программа осуществляет простую проверку опций командной строки, чтобы решить, как вести себя плохо: -Ь вызывает доступ к памяти за ее выделенными границами, a -f пытается использовать освобожденную память. (Строки 18 и 21 являются соответственно опасными.) Обратите внимание, что без опций указатель никогда не освобождается (строка 25); Electric Fence не перехватывает этот случай. Одним из способов использования Electric Fence, способом, который гарантированно работает на различных системах Unix и GNU/Linux, является статическая компоновка с ним вашей программы. Затем программа должна быть запущена из отладчика. (Документация Electric Fence явно указывает, что Electric Fence не следует компоновать с дво-
15.5. Отладочные инструменты 579 ичным файлом готового изделия.) Следующий сеанс демонстрирует эту процедуру и показывает, что происходит для обоих опций командной строки: $ ее -g chl5-badmanl.c -lefence -о chl5-badmeml /* Откомтилировать; компановка статическая */ $ gdb chl5-badmeml /* Запустить из отладчика */ GNU gdb 5.3 (gdb) run -b /* Попробовать опцию -b */ Starting program: /home/arnold/progex/code/chl5/chl5-badmeml -b [New Thread 8192 (LWP 28021)] Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com> p = <not 30 bytes> Program received signal SIGSEGV, Segmentation fault. SIGSEGV: GDB prints where [Switching to Thread 8192 (LWP 28021)] 0x080485b6 in main (argc=2, argv=0xbffff8a4) at chl5-badmeml.c:18 18 p[42] = 'a'; /* коснуться за пределами границы */ (gdb) run -f /* Теперь попробовать опцию -f */ The program being debugged has been started already. Start it from the beginning? (y or n) у /* Да */ Starting program: /home/arnold/progex/code/chl5/chl5-badmeml -f [New Thread 8192 (LWP 28024)] Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com> p = <not 3 0 bytes> Program received signal SIGSEGV, Segmentation fault. /* Снова SIGSEGV */ [Switching to Thread 8192 (LWP 28024)] 0x080485e8 in main (argc=2, argv=0xbffff8a4) at chl5-badmeml.с:21 21 p[0] = 'b'; На системах, которые поддерживают разделяемые библиотеки и переменную окружения LD_PRELOAD (в том числе и на GNU/Linux), вам не нужно явным образом компоновать библиотеку ef епсе. Вместо этого сценарий оболочки ef организует запуск программы с соответствующей настройкой. Хотя мы не описали механизмы подробно, GNU/Linux (и другие системы Unix) поддерживают разделяемые (shared) библиотеки, особые версии библиотечных процедур, которые хранятся в одном файле на диске, вместо того, чтобы копироваться в каждый отдельный двоичный исполняемый файл программы. Разделяемые библиотеки экономят дисковое пространство и могут сохранить системную память, поскольку все программы, использующие разделяемые библиотеки, используют одну и ту же копию библиотеки в памяти. Платой за это является замедление загрузки программы, поскольку программу и разделяемую библиотеку нужно подключить друг к другу прежде, чем программа сможет начать выполнение. (Обычно это прозрачно для вас, пользователя.) Переменная окружения LD_PRELOAD заставляет системный загрузчик программ (который загружает исполняемые файлы в память) связаться со специальной библиотекой до стандартных библиотек. Сценарий ef использует эту особенность для связывания
580 Глава 15. Отладка набора функций mallocO в Electric Fence . Таким образом, повторная компоновка даже не нужна. Этот пример демонстрирует ef: $ ее -g chl5-badmeml.c -о chl5-badmeml /* Компилировать как обычно */ $ ef chlS-badmeml -b /* Запустить с использованием ef, создает дамп ядра */ Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens <bruce@perens.com> p = <not 3 0 bytes> /usr/bin/ef: line 20: 28005 Segmentation fault (core dumped) ( export LD_PRELOAD=libefence.so.0.0; exec $* ) $ ef chlS-badmeml -f /* Запустить с использованием ef, снова создает дамп ядра */ Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens <bruce@perens.com> p = <not 30 bytes> /usr/bin/ef: line 20: 28007 Segmentation fault (core dumped) ( export LD_PRELOAD=libefence.so.0.0; exec $* ) $ Is -1 core* /* Linux создает для нас разные файлы core */ -rw 1 arnold devel 217088 Aug 28 15:40 core.28005 _rw 1 arnold devel 212992 Aug 28 15:40 core.28007 GNU/Linux создает файлы core, которые включают в свое имя ID процесса. В данном случае такое поведение полезно, поскольку мы можем отдельно отлаживать каждый файл core: $ gdb chl5-badmeml core.28005 /* От опции -b */ . GNU gdb 5.3 Core was generated by xchl5-badmeml -b'. Program terminated with signal 11, Segmentation fault. #0 0x08048466 in main (argc=2, argv=0xbffff8c4) at chl5-badmeml.с:18 18 p[42] = 'a'; /* touch outside the bounds */ (gdb) quit $ gdb chl5-badmeml core.28007 /* От опции -f */ GNU gdb 5.3 Core was generated by Nchl5-badmeml -f ' . Program terminated with signal 11, Segmentation fault. #0 0x08048498 in main (argc=2, argv=0xbffff8c4) at chl5-badmeml.с:21 21 p[0] = 'b'; 9 GDB также позволяет определить переменную окружения LDPRELOAD: set environment LD_PRELOAD=PATHJTO_YOUR_LIBRARY. - Примеч. науч. ред.
15.5. Отладочные инструменты 581 Справочная страница efenceC) описывает несколько переменных окружения, которые должны быть установлены, чтобы настроить поведение Electric Fence. Следующие три наиболее примечательны. EF_PROTECT_BELOW • Установка этой переменной в 1 заставляет Electric Fence проверять «недоборы» (underruns) вместо «переборов» (overruns) при выходе за пределы отведенной памяти. «Перебор», т.е. доступ к памяти в области за выделенной, был продемонстрирован ранее. «Недобор» является доступом к памяти, расположенной перед выделенной областью памяти. EF_PROTECT_FREE Установка этой переменной в 1 предотвращает повторное использование Electric Fence памяти, которая была корректно освобождена. Это полезно, когда вы думаете, что программа может получать доступ к освобожденной памяти; если освобожденная память впоследствии была выделена заново, доступ к ней через предыдущий висячий указатель остался бы в противном случае незамеченным. EF_ALLOW_MALLQC_0 При наличии ненулевого значения Electric Fence допускает вызовы 'malloc(O)'. Такие вызовы в стандартном С технически действительны, но могут представлять программную ошибку. Соответственно Electric Fence по умолчанию их запрещает. Вдобавок к переменным окружения Electric Fence предоставляет глобальные переменные с такими же названиями. Вы можете изменить их значения из отладчика, так что можно динамически изменять поведение программы, которая уже начала выполнение. Подробности см. в efenceC). 15.5.2.3. ОтладкаMalloc:dmalloc Библиотека dmalloc предоставляет большое число опций отладки. Ее автором является Грей Ватсон (Gray Watson), есть также и свой веб-сайт20. Как и в случае с Electric Fence, она может быть уже установленной на вашей системе, или же вы можете ее извлечь и построить самостоятельно. Библиотека dmalloc проверяет наличие в переменной окружения DMALLOC_OPTIONS управляющей информации. Например, она может выглядеть следующим образом: $ echo $DMALLOC_OPTIONS debug=0x4e40503,inter=100/log=dm-log Компонент 'debug' этой переменной содержит набор битовых флагов, которыми для большинства людей почти невозможно непосредственно управлять. Поэтому документация описывает двухэтапный процесс для облегчения их использования. Первый шаг заключается в определении функции оболочки с названием dmalloc, которая вызывает программу драйвера dmalloc: http : / /www. dmal loc .com - Примеч. автора.
582 Глава 15. Отладка $ dmalloc () { > eval 'command dmalloc -b $*x /* Команда 'command7 обходит функции оболочки */ > } После того, как это сделано, вы можете передать функции опции для установки файла журнала (-1), указать число итераций, после которых dmalloc должна проверить свои внутренние структуры данных (-i), и указать уровень отладки или другой тэг ('low'): $ dmalloc -l dm-log -i 100 low Как и Electric Fence, библиотека dmalloc может быть скомпонована с приложением статически или связана динамически при помощи LD_PRELOAD. Последнее демонстрирует следующий пример: $ LD_PRELOAi)=libdmalloc.so chl5-badmeml -b /* Запустить с проверкой */ р = <not 3 0 bytes> /* Показан нормальный вывод */ ЗАМЕЧАНИЕ. Не используйте 'export LD__PRELOAD=libdmalloc. so'! Если вы это сделаете, каждая программа, которую вы запустите, тдкая как Is, будет выполняться со включенной проверкой malloc (). Ваша система быстро станет непригодной. Если вы сделали это случайно, можете использовать 'unset LD^PRELOAD', чтобы восстановить обычное поведение. Результаты записываются в файл dm-log следующим образом: $ cat dm-log Dmalloc version '4.8.1' from 'http://dmalloc.com/' flags = 0x4e40503, logfile 'dm-log' interval = 100, addr = 0, seen # = 0 starting time = 1062078174 free bucket count/bits: 63/6 basic-block 4056 bytes, alignment 8 bytes, heap grows up heap: 0x804a000 to 0x804d000, size 12288 bytes C blocks) heap checked 0 alloc calls: malloc 1, calloc 0, realloc 0, free 0 alloc calls: recalloc 0, memalign 0, valloc 0 total memory allocated: max in use at one time: max alloced with 1 call: max alloc rounding loss: max memory space wasted: final user memory space: 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174: 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174: 1062078174 1062078174 1062078174 1062078174 1062078174 1062078174 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: 1: ¦ 1: : 1: 1: 4062 bytes 3 0 bytes A pnts) 30 bytes A pnts) 3 0 bytes 34 bytes E3%) 3998 bytes (98%) basic 0, divided 1, final admin overhead: basic 1, divided 1, 8192 bytes F6%) final external space: 0 bytes @ blocks) top 10 allocations: total-size count in-use-size count source 30 1 30 1 ra=0x8048412 30 1 30 1 Total of 1 dumping not-freed pointers changed since 0:
15.5. Отладочные инструменты 583 1062078174: 1: not freed: '0х804с008|si' C0 bytes) from 'ra=0x8048412' 1062078174: 1: total-size count source 1062078174: 1: 30 1 Га=0х8048412 /* Выделение здесь */ 1062078174: 1: 30 1 Total of 1 1062078174: 1: unknown memory: 1 pointer, 30 bytes 1062078174: 1: ending time = 1062078174, elapsed since start = 0:00:00 Вывод содержит много статистических данных, которые нам пока не интересны. Интересна строка, в которой указывается не освобожденная память, с адресом возврата, указывающим на выделившую память функцию Dга=0х8048412'). Документация dmalloc объясняет, как получить расположение в исходном коде этого адреса с использованием GDB: $ gdb chl5-badmeml /* Запустить GDB */ GNU gdb 5.3 (gdb) -x 0x8048412 /* Проверить адрес */ 0x8048412 <main+26>: 0x8910c483 (gdb) info line *@x8048412) /* Получить сведения о строке */ Line 11 of "chlS-badmeml.c" starts at address 0x8048408 <main+16> and ends at 0x8048418 <main+32>. Это трудно, но выполнимо, если нет другого выбора. Однако, если вы включите в свою программу заголовочный файл "dmalloc.h" (после всех остальных операторов #include), вы можете получить сведения из исходного кода непосредственно в отчете: 1062080258 1062080258 1Q62080258 1062080258 1062080258 1062080258: 1062080258 1062080258 1062080258 top 10 allocations: total-size count in-use-size count source 30 1 30 1 chl5-badmem2.c:13 30 1 30 1 Total of 1 dumping not-freed pointers changed since 0: 1: not freed: '0x804c008|si' C0 bytes) from 'chlS-badmenu.crU' 1: total-size count source 1: 30 1 chl5-badmem2.c:13 1: 30 1 Total of 1 (Файл chl5-badrnem2 .с является аналогичным chl5-badmeml.c, за исключением того, что он включает "dmalloc . h", поэтому мы не стали беспокоиться с его отображением.) Отдельные возможности отладки включаются или выключаются посредством использования лексем (tokens) - специально распознаваемых идентификаторов - и опций -р для добавления лексем (свойств) или -т для их удаления. Имеются предопределенные комбинации, 'low', 'med' и 'high'. Чем являются эти комбинации, вы можете увидеть с помощью 'dmalloc -Lv': $ dmalloc low /* Установить low */ $ dmalloc -Lv /** Показать установки */ Debug Malloc Utility: http://dmalloc.com/
584 Глава 15. Отладка For a list of the command-line options enter: dmalloc --usage Debug-Flags 0x4e40503 (82052355) (low) /* Текущие лексемы */ log-stats, log-non-free, log-bad-space, log-elapsed-time, check-fence, free-blank, error-abort, alloc-blank, catch-null Address not-set Interval 100 Lock-On not-set Logpath 'log2' Start-File not-set Полный список лексем вместе с кратким объяснением и соответствующим каждой лексеме числовым значением можно получить с помощью 'dmalloc -DV': $ dmalloc -DV Debug Tokens: none (nil) -- no functionality @) log-stats Ast) -- log general statistics @x1) log-non-free (lnf) -- log non-freed pointers @x2) log-known (lkn) -- log only known non-freed @x4) log-trans (ltr) -- log memory transactions @x8) log-admin (lad) -- log administrative info @x20) log-blocks (lbl) -- log blocks when heap-map @x40) log-bad-space (lbs) -- dump space from bad pnt @x100) log-nonfree-space (Ins) -- dump space from non-freed pointers @x200) log-elapsed-time (let) — log elapsed-time for allocated pointer @x40000) log-current-time (let) — log current-time for allocated pointer @x80000) check-fence (cfe) -- check fence-post errors @x400) check-heap (che) -- check heap adm structs @x800) check-lists (cli) -- check free lists @x1000) check-blank (cbl) — check mem overwritten by* alloc-blank, free-blank @x2000) check-funcs (cfu) -- check functions @x4000) force-linear (fli) -- force heap space to be linear @x10000) catch-signals (csi) — shutdown program on SIGHUP, SIGINT, SIGTERM @x20000) realloc-copy (rco) -- copy all re-allocations @x100000) free-blank (fbl) — overwrite freed memory space with BLANK_CHAR @x200000) error-abort (eab) -- abort immediately on error @x400000) alloc-blank (abl) — overwrite newly alloced memory with BLANK_CHAR @x800000) heap-check-map (hem) --log heap-map on heap-check @x1000000) print-messages (pme) -- write messages to stderr @x2000000) catch-null (cnu) -- abort if no memory available @x4000000) never-reuse (nre) -- never re-use freed memory @x8000000) allow-free-null (afn) -- allow the frees of NULL pointers @x20000000) error-dump (edu) -- dump core on error and then continue @x40000000)
15.5. Отладочные инструменты 585 К этому времени у вас должно быть ощущение того, как использовать dmalloc, и его гибкости, dmalloc является избыточным для нашей простой демонстрационной программы, но он неоценим для более крупномасштабного, реального приложения. 15.5.2.4. Valgrind: многосторонний инструмент Инструменты, описанные в предыдущем разделе, все фокусируются на отладке динамической памяти, и это в самом деле является значительной проблемной областью для многих программ. Однако, проблемы динамической памяти не являются единственной разновидностью. Программа Valgrind под лицензией GPL охватывает большое разнообразие проблем, включая те, которые происходят от динамической памяти. Руководство по Valgrind описывает программу также или лучше, чем можем мы, поэтому мы будем цитировать (и сокращать) его по мере продвижения вперед. Valgrind является гибким инструментом для отладки и профилирования исполняемых файлов Linux-x86. Инструмент состоит из ядра, которое программно обеспечивает искусственный процессор х86, и ряда «оболочек», каждая из которых является отладочным или профилирующим инструментом. Архитектура модульная, так что можно легко создавать новые «оболочки», не нарушая существующую структуру. Наиболее полезной «оболочкой» является memcheck. «Оболочка» memcheck обнаруживает в ваших программах проблемы с управлением памятью. Проверяются все чтения и записи памяти, а вызовы malloc /new/ free/delete перехватываются. В результате memcheck может обнаружить следующие проблемы: • Использование неинициализированной памяти. • Чтение/запись в память после ее освобождения. • Чтение/запись за границей выделенного malloc блока. • Чтение/запись в ненадлежащие области стека. • Утечки памяти, когда указатели на выделенные malloc теряются навсегда. • Несоответствующее использование malloc/new/new [] против free/delete/ delete []. • Некоторые неправильные употребления pthreads API POSIX. Проблемы, подобные этим, могут быть трудно обнаруживаемыми другими средствами, часто остающимися необнаруженными в течение длительного времени и вызывающими редкие, трудные для обнаружения отказы. Другие «оболочки» более специализированы: • cachegrind осуществляет обстоятельную имитацию кэшей II, D1 и L2 процессора, поэтому может точно указать источники осечек кэшей в вашем коде. • addrcheck идентична memcheck за исключением одной детали - она не проверяет неинициализированные данные. Все остальные проверки - главным образом, точная проверка адресов - по-прежнему проводится. Обратной стороной этого является то, что вы не перехватываете ошибки неинициализированных данных, которые может найти memcheck.
586 Глава 15. Отладка Но положительная сторона значительна: программы работают почти в два раза быстрее, чем с memcheck, используя значительно меньше памяти. Утилита по-прежнему находит чтения/записи освобожденной памяти, памяти за пределами выделенных блоков и в других недействительных местах, ошибки, которые вы действительно хотите обнаружить до выпуска программы в свет! • helgrind является отладочной оболочкой, предназначенной для обнаружения состязания данных в многопоточных программах. Наконец, руководство отмечает: Valgrind тесно связан с особенностями процессора, операционной системы и, в меньшей степени, компилятора и основных библиотек С. Это затрудняет его переносимость, поэтому мы с самого начала сконцентрировались на том, что мы считаем широко использующейся платформой: Linux на х86. Valgrind использует стандартный механизм Unix 4. /configure', 'make', 'make install', и мы попытались обеспечить его работу на машинах с ядром 2.2 или 2.4 и glibc 2.1.X, 2.2.Х или 2.3.1. Это должно охватить значительное большинство современных установок Linux. Обратите внимание, что glibc-2.3.2+ с пакетом NPTL (Native POSIX Thread Library - собственная библиотека потоков POSIX) не будет работать. Мы надеемся исправить это, но это будет нелегко. Если вы используете GNU/Linux на другой платформе или используете коммерческую систему Unix, Valgrind не окажет вам большой помощи. Однако, поскольку системы GNU/ Linux на х86 довольно обычны (и вполне доступны), вполне вероятно, что вы сможете приобрести ее с умеренным бюджетом, или по крайней мере, занять на время! Что еще, когда Valgrind нашел для вас проблему, она исправляется для любой платформы, для которой компилируется ваша программа. Таким образом, разумно использовать систему х86 GNU/Linux для разработки, а какую-нибудь другую коммерческую систему Unix для развертывания высококачественного продукта21. Хотя из руководства Valgrind у вас могло сложиться впечатление, что существуют отдельные команды memcheck, addrcheck и т. д., это не так. Вместо этого программа оболочки драйвера с именем valgrind запускает отладочное ядро с соответствующей «оболочкой», указанной в опции --skin=. Оболочкой по умолчанию является memcheck; таким образом, запуск просто valgrind равносильно Valgrind --skin=memcheck\ (Это обеспечивает совместимость с более ранними версиями Valgrind, которые осуществляли лишь проверку памяти, это имеет также больший смысл, поскольку оболочка memcheck предоставляет большую часть сведений.) Valgrind предусматривает ряд опций. За всеми подробностями мы отсылаем вас к его документации. Опции поделены на две группы; из тех, которые используются с ядром (т. е. работают для всех оболочек), наиболее полезными могут быть следующие: Все в большей степени для разработки высококачественных продуктов используется также GNU/Linux! - Примеч. автора.
15.5. Отладочные инструменты 587 -- gdb-attach=no/yes Запускается с подключенным к процессу GDB для интерактивной отладки. По умолчанию используется по. --help Перечисляет опции. --logfile=файл Записывает сообщения в фаш.р1й. --num-callers = чиел о Выводит число вызывающих в трассировке стека. По умолчанию 4. --skin=оболочка ¦ Использует соответствующую оболочку. По умолчанию memcheck. --trace-children=no|yes Запускает трассировку также в порожденных процессах. По умолчанию используется по. -v, --verbose Использует более полный вывод. Это включает перечисление загруженных библиотек, а также подсчеты всех различных видов ошибок. Из опций для оболочки memcheck мы полагаем, что эти являются наиболее полезными: --leak-check=no|yes Искать утечки памяти после завершения программы. По умолчанию используется по. --show-reachable=no|yes Показать доступные блоки после завершения программы. Если используется --show-reachable=yes, Valgrind ищет динамически выделенную память, на которую все еще есть указывающий на нее указатель. Такая память не является утечкой, но о ней все равно следует знать. По умолчанию используется по. Давайте посмотрим на Valgrind в действии. Помните chl5-badmem.c? (См. раздел 15.5.2.2 «Electric Fence».) Опция -b записывает в память, находящуюся вне выделенного та 11 ос () блока. Вот что сообщает Valgrind: $ valgrind chl5-badmeml -b 1 ==8716== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux. 2 ==8716== Copyright (C) 2002-2003, and GNU GPL'd, by Julian Seward. 3 ==8716== Using valgrind-20030725, a program supervision framework for x86-linux. 4 ==8716== Copyright (C) 2000-2003, and GNU GPL'd, by Julian Seward. 5 ==8716== Estimated CPU clock rate is 2400 MHz 6 ==8716== For more details, rerun with: -v 7 ==8716== 8 p = <not 30 bytes> 9 ==8716== Invalid write of size 1 10 ==8716== at 0x8048466: main (chl5-badmeml.с:18) 11 ==8716== by 0x420158D3: libc_start_main (in /lib/i686/libc-2 .2 .93 .so) 12 ==8716== by 0x8048368: (within /home/arnold/progex/code/chl5/chl5-badmeml) 13 ==8716== Address 0x4104804E is 12 bytes after a block of size 30 alloc'd
588 Глава 15. Отладка 14 ==8716== at 0x40025488: malloc (vg_replace_malloc.с:153) 15 ==8716== by 0x8048411: main (chl5-badmeml.с:11) 16 ==8716== by 0x420158D3: libc_start_main (in /lib/i686/libc-2.2.93 .so) 17 ==8716== by 0x8048368: (within /hoine/arnold/progex/code/chl5/chl5-badmeml) 18 ==8716== 19 ==8716== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) 20 ==8716== malloc/free: in use at exit: 30 bytes in 1 blocks. 21 ==8716== malloc/free: 1 allocs, 0 frees, 30 bytes allocated. 22 ==8716== For a detailed leak analysis, rerun with: --leak-check=yes 23 ==8716== For counts of detected errors, xerun with: -v (Были добавлены номера строк в выводе, чтобы облегчить обсуждение.) Строка 8 является выводом программы; остальные от Valgrind в стандартную ошибку. Сообщение об ошибке находится в строках 9-17. Она указывает, сколько байтов было записано неверно (строка 9), где это случилось (строка 10), и показывает трассировку стека. Строки 13-17 описывают, откуда была выделена память. Строки 19-23 подводят итоги. Опция -f программы chl5-badmeml освобождает выделенную память, а затем записывает в нее через висячий указатель. Вот что сообщает Valgrind в этом случае: $ valgrind chl5-badmeml -f ==8719== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux. p = <not 3 0 bytes> ==8719==_lnvalid write of size 1 ==8719== at 0x8048498: main (chl5-badmeml.c:21) ==8719== by 0x420158D3: libc_start_main (in /lib/i686/libc-2.2.93.so) ==8719== by 0x8048368: (within /home/arnold/progex/code/chl5/chl5-badmeml) ==8719== Address 0x41048024 is 0 bytes inside a block of size 30 free'd ==8719== at 0x40025722: free (vg_replace_malloc.с:220) ==8719== by 0x8048491: main (chl5-badmeml.с:20) ==8719== by 0x420158D3: libc_start_main (in /lib/i686/libc-2.2.93.so) ==8719== by 0x8048368: (within /home/arnold/progex/code/chl5/chl5- badmeml) На этот раз в отчете указано, что запись была осуществлена в освобожденную память и что вызов free () находится в строке 20 chl5-badmeml .с. При вызове без опций chl5-badmeml. с выделяет и использует память, но не освобождает ее. О таком случае сообщает опция --leak-check=yes: $ valgrind --leak-check=yes chl5-badmeml 1 ==8720== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux. 8 p = <not 30 bytes> 9 ==8720== 10 ==8720== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
15.5. Отладочные инструменты 589 11 ==8720== malloc/free: in use at exit: 30 bytes in 1 blocks. 12 ==8720== malloc/free: 1 allocs, 0 frees, 30 bytes allocated. 16 ==8720== 17 ==8720== 30 bytes in 1 blocks are definitely lost in loss record 1 of 1 18 ==8720== at 0x40025488: malloc (vg_replace_malloc.с:153) 19 ==8720== by 0x8048411: main (chl5-badmeml.с:11) 20 ==8720== by 0x420158D3: libc__start_main (in /lib/i686/libc-2 .2 .93 .so) 21 ==8720== by 0x8048368: (within /home/arnold/progex/code/chl5/chl5-badmeml) 22 ==8720== 23 ==8720== LEAK SUMMARY: 24 ==8720== definitely lost: 3 0.bytes in 1 blocks. 25 ==8720== possibly lost: 0 bytes in 0 blocks. 26 ==8720== still reachable: 0 bytes in 0 blocks. 27 ==8720== suppressed: 0 bytes in 0 blocks. 28 ==8720== Reachable blocks (those to which a pointer was found) are not shown. 2 9 ==872 0== To see them, rerun with: --show-reachable=yes Строки 17-29 предоставляют отчет об утечке; эта память была выделена в строке 11 chl5-badmeml.с. Помимо отчетов о неправильном использовании динамической памяти, Valgrind может диагностировать использование неинициализированной памяти. Рассмотрим следующую программу, chl5-badmem3 .с: I /* chl5-badmem3.с плохое обращение с нединамической памятью */ 2 3 #include <stdio.h> 4 #include <stdlib.h> 5 6 int main(int argc, char **argv) 7 { 8 int a_var; /* Обе не инициализированы */ 9 int b_var; 10 II /* Valgrind не отметит это; см. текст. */ 12 a_var .= b_var; 13 14 /* Использование неинициализированной памяти; это отмечается. */ 15 printf("a_var = %d\n", a_var); 16 17 return 0; 18 } При запуске Valgrind выдает этот (сокращенный) отчет: ==29650== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.
590 Глава 15. Отладка ==29650== Use of uninitialised value of size 4 ==29650== at 0x42049D2A: _IO_vfprintf„internal (in /lib/i686/libc-2 .2 .93 .so) ==29650== by 0x420523Cl: _IO_printf (in /lib/i686/libc-2.2.93.so) ==29650== by 0x804834D: main (chl5-badmem3.с:15) ==29650== by 0x420158D3: libc_start_main (in /lib/i686/libc-2 .2 .93 .so) ==29650== ==29650== Conditional jump or move depends on uninitialised value(s) ==29650== at 0x42049D32: _IO_vfprintf_internal (in /lib/i686/libc-2.2.93.so) ==29650== by 0x420523Cl: _IO_printf (in /lib/i686/libc-2.2.93.so) ==29650== by 0x804834D: main (chl5-badmem3.с:15) ==29650== by 0x420158D3: libc_start_main (in /lib/i686/libc-2 .2 .93 .so) a__var = 1107341000 ==29650== ==29650== ERROR SUMMARY: 25 errors from 7 contexts (suppressed: 0 from 0) ==29650== malloc/free: in use at exit: 0 bytes in 0 blocks. ==29650== malloc/free: 0 allocs, 0 frees, 0 bytes allocated. ==29650== For a detailed leak analysis, rerumwith: --leak-check=yes ==29650== For counts of detected errors, rerun with: -v В документации Valgrind объясняется, что копирование неинициализированных данных не выдает сообщений об ошибках. Оболочка memcheck отмечает состояние данных (неинициализированные) и отслеживает его при перемещениях данных. Таким образом, a_var считается неинициализированной, поскольку это значение было получено от b_var, которая была неинициализированной. memcheck сообщает о проблеме лишь тогда, когда неинициализированное значение используется. Здесь это происходит в библиотеке С (_IO_vfprintf_internal ()), которая должна преобразовать значение в строку; для этого, она проводит с этим значением вычисления. К сожалению, хотя Valgrind может обнаружить использование неинициализированной памяти вплоть до уровня битов, он не может осуществлять проверки границ массивов для локальных и глобальных переменных. (Valgrind может осуществлять проверку границ для динамической памяти, поскольку он сам обрабатывает такую память, поэтому знает о начале и конце каждой области.) В заключение, Valgrind является мощным инструментом отладки памяти. Он использовался в таких крупномасштабных, многопоточных производственных программах, как KDE 3, OpenOffice и веб-браузер Konquerr. Он конкурирует с несколькими коммерческими предложениями, а другая его версия была даже использована (совместно с эмулятором WINE22) для отладки программ, написанных для Microsoft Windows с использованием Visual C++! Вы можете получить Valgrind с его веб-сайта23. 22 http : / /www. winehq. com - Примеч. автора http: / /valgrind. kde . org - Примеч. автора.
15.5. Отладочные инструменты 591 15.5.2.5. Другие отладчики malloc Две статьи Cal Ericson в Linux Journal описывают mtrace и dmalloc, а также большинство других перечисленных ниже инструментов. Эти статьи Memory Leak Detection in Embedded Systems, выпуск 10124, сентябрь 2002 г., и Memory Leak Detection in C++, выпуск НО25, июнь 2003 г. Обе статьи доступны на веб-сайте Linux Journal. Другие инструменты сходны по природе с описанными ранее. ccmalloc Замещающая malloc () библиотека, которая не нуждается в особой компиляции и может использоваться с C++. См. http://www.inf.ethz.ch/personal/ biereyprojects/ccmalloc. malloc Марка Мораеса (Mark Moraes) Старинная, но полнофункциональная библиотека замещения malloc (), предоставляющая возможности профилирования, трассировки и отладки. Вы можете получить ее сftp://ftp.cs.toronto. edu/pub/moraes/malloc-1.18.tar.gz. mpatrol Пакет с большими возможностями настройки для отладки памяти и тестирования. См. http: / /www. cbmamiga. demon. со. uk/mpatrol. memwatch Пакет, требующий использования специального заголовочного файла и опций времени компилирования. См. http: //www. linkdata. se/sourcecode.html. njamd «He просто еще один отладчик malloc» (Not Just Another Malloc Debugger). Эта библиотека не требует специальной компоновки с приложением; вместо этого она использует LD_PRELOAD для замены стандартных процедур. См. http: //source- forge .net/projects/njamd. yamd Похож на Electric Fence, но Со многими дополнительными опциями. См. http: // www3.hmc.edu/~ne1dredge/yamd. Почти все из этих пакетов используют для точной настройки своего поведения переменные окружения. В таблице 15.1 на основе статей из Linux Journal сделана сводка различных пакетов. Как видно, для отладки проблем динамической памяти доступен ряд выборов. На системах GNU/Linux и BSD один или более из этих инструментов, возможно, уже установлены, что избавляет вас от хлопот по их загрузке и построению. http: //www. linuxjournal. com/article.php?sid=6059 -Примеч. автора http: //www. linuxjournal. com/article .php?sid=6556 -Примеч. автора
592 Глава 15. Отладка Таблица 15.1. Сводка особенностей инструментов памяти Инструмент ОС Заголовочный файл Модуль/ программа Мн о го- поточность ccinalloc dmalloc memwatch Moraes mpatrol mtrace njamd valgrind yamd Многотипная Многотипная ( Многотипная Многотипная Многотипная Linux (GLIBC) Многотипная Linux (GLIBC) Linux, DJGPP Нет Необязательно Да Необязательно Нет Да Нет Нет Нет Программа Программа Программа Программа Программа Программа Модуль Программа Программа Программа Нет Да Нет Нет Нет Да Нет Нет Да Нет Полезно также использовать для своей программы Например, mtrace для обнаружения не освобождаемой хвата доступа к недействительной памяти. несколько инструментов подряд, памяти, a Electric Fence для пере- 15.5.3. Современная lint В оригинальном С компилятор не мог проверить, соответствуют ли параметры, переданные в вызове функции, списку параметров в определении функции; прототипов не было. Это часто вело к неуловимым ошибкам, поскольку ошибочный вызов функции мог вызывать лишь частично ошибочные результаты, которые проходили незамеченными во время тестирования, или мог даже вообще не появиться во время тестирования. Например: if (argc < 2) fprintf ("usage: %s [ options ] files\n", argv[0]); /* отсутствует stderr */ Если программа, содержащая этот фрагмент, никогда не вызывается с ошибочным числом аргументов, f printf (), в которой отсутствует первый аргумент FILE *, также никогда не вызывается. Программа V7 lint была предназначена для решения таких проблем. Она делала два прохода через все файлы программы, сначала собирая сведения об аргументах функций, а затем сравнивая вызовы функций с собранной информацией. Особые файлы «библиотеки lint» предоставляли сведения о функциях стандартных библиотек, так что их также можно было проверить lint проверяла также другие сомнительные конструкции. С появлением в стандартном С прототипов необходимость в lint уменьшилась, но не исчезла совсем, поскольку С89 все еще допускает объявления функций в старом стиле: extern int some_func(); /* Список аргументов неизвестен */
15.6. Тестирование программ 593 Вдобавок, многие другие аспекты программы можно проверять статически, т .е. путем анализа исходных текстов. Программа splint (Secure Programming Lint - Lint для безопасного программированияJ6 является совеременным обновлением lint. Она предусматривает слишком много опций и возможностей, чтобы перечислять их здесь, но ее стоит исследовать. Следует знать об одной особенности подобных lint программ, которая заключается в том, что они выводят целый поток предупреждающих сообщений. Многие из сообщаемых предупреждений в действительности безвредны. В таких случаях инструменты допускают использование специальных комментариев, сообщающих: «Да, я знаю об этом, это не проблема», splint лучше всего работает, когда вы предоставляете в своем коде большое количество подобных примечаний. splint является мощным, но сложным инструментом; выделение некоторого времени на изучение его использования, а затем частое его использование поможет сохранить ваш код ясным. 15.6. Тестирование программ Разработка программного обеспечения содержит элементы и искусства, и науки; это одна сторона того, что делает ее такой восхищающей и стимулирующей профессией. Данный раздел вводит в тему тестирования профаммного обеспечения, которая также включает в себя и искусство, и науку; таким образом, это несколько более общий и высокий уровень (читай: «на который можно махнуть рукой»), чем остальная часть данной главы. Тестирование программ является неотъемлемой частью процесса разработки программного обеспечения. Весьма маловероятно, что программа заработает правильно на 100 процентов при первой компиляции. Программа не несет ответственности за свою правильность; за это отвечает автор программы. Одним из самых важных способов проверки того, что программа работает так, как предполагалось, является ее тестирование. Один из способов классификации различных видов тестов следующий: Тесты модулей (Unit tests) Это тесты, которые вы пишете для каждого модуля или функционального компонента своей программы. В качестве части работы может потребоваться также создать окружение (scaffolding) - код, предназначенный для предоставления поддерживающего каркаса, чтобы запустить модуль в виде отдельной пррграммы. Важно спроектировать тесты для каждого функционального компонента во время его разработки. Это помогает прояснить проектирование особенностей; знание того, как это тестировать, помогает определить, что следует и что не следует делать в первую очередь. ht tp: / /www. spl int. org - Примеч. автора. 20 -159
594 Глава 15. Отладка Комплексные тесты (Integration tests) Это тесты, которые применяются, когда все функциональные компоненты были написаны, протестированы и отлажены по отдельности. Идея в том, что все затем помещается на свое место в каркасе и тестируется все в целом, чтобы убедиться, что взаимодействия между компонентами работают. Возвратные тесты (Regression tests) Неизбежно вы (или ваши пользователи!) обнаружат проблемы. Это могут быть действительные ошибки, или ограничения дизайна, или неизбежные отказы в «пограничных случаях». Когда вы смогли воспроизвести и исправить проблему, сохраните первоначальные условия отказа в качестве возвратного теста. Возвратный тест позволяет вам убедиться, что при проведении изменений не была повторена старая проблема. (Это случается довольно легко.) Пропустив программу после проделанных изменений через набор тестов, вы можете быть (более) уверены, что все работает таким образом, как предполагалось. Тестирование следует по возможности автоматизировать. Это особенно легко сделать для программ, не содержащих графического пользовательского интерфейса (GUI), написанных в стиле инструментов Linux/Unix: читающих стандартный ввод или указанные файлы и записывающих в стандартный вывод и стандартную ошибку. По меньшей мере, тестирование можно осуществить с помощью простых сценариев оболочки. Более сложное тестирование осуществляется обычно с помощью отдельного подкаталога test и программы make. Тестирование программного обеспечения само по себе является отдельной областью, и мы не предполагаем отдавать ей здесь должное; скорее, наше намерение дать вам знание, что тестирование является неотъемлемой частью разработки и часто движущей силой для использования ваших навыков в отладке! Вот очень короткий резюмирующий список: Проектируйте тест вместе с функциональностью. , Тестируйте пограничные условия: убедитесь, что функция работает внутри и на действительных границах и что она корректно выдает ошибку за их пределами. (Например, функция sqrt () должна потерпеть неудачу с отрицательным аргументом.) Используйте в своем коде операторы проверки (см. раздел 12.1 «Операторы проверки: assert () ») и проведите свои тесты с разрешенными операторами проверки. • Создайте и используйте повторно тестовое окружение. • Сохраняйте условия сбоев для возвратного тестирования. Как можно больше автоматизируйте тестирование. Печатайте число потерпевших неудачу тестов, чтобы легко можно было определить успех или неудачу, а также степень неудачи. • Используйте инструменты обзора кода, такие, как gcov, чтобы удостовериться, что ваш набор тестов охватывает весь ваш код. Тестируйте с самого начала и тестируйте часто. Изучите литературу по тестированию программного обеспечения, чтобы совершенствовать свою способность разрабатывать и тестировать программное обеспечение.
15.7. Правила отладки 595 15.7. Правила отладки Отладка не является «черной магией». Ее принципы и методики могут быть изучены и последовательно применены каждым. С этой целью мы рекомендуем книгу Debugging Дэвида Эганса (David J. Agans; ISBN: 0-8144-7168-4). У книги есть веб-сайт27, на котором обобщены правила и представлен плакат для загрузки, чтобы вы могли его распечатать и повесить на стену в своем офисе. Чтобы завершить наше обсуждение, мы представляем следующий материал. Он был адаптирован Дэвидом Эгансом по разрешению из Debugging, Copyright © 2002 David J. Agans, опубликованной АМАСОМ28, отделением American Management Association, New York. Мы благодарим его. 1. Поймите систему. Когда ничто не помогает, прочтите руководство. Вам необходимо узнать, что должна делать проблемная система и все ее части, если хотите выяснить, почему она не работает. Поэтому прочтите всю документацию, которую можете получить в свои руки (и в свой браузер). Знание того, где находятся функциональные блоки и размещаются данные, и как они взаимодействуют, дает вам схему для изоляции ошибки. Конечно, вам нужно знать также соответствующую область (язык, операционную систему, приложение) и свои инструменты (компилятор, отладчик исходного кода). 2. Вызовите сбой. Для того, чтобы увидеть ошибку, вы должны быть способны постоянно воспроизводить сбой. Задокументируйте свои процедуры и начните с известного состояния, чтобы вы всегда могли снова вызвать сбой. Ищите ошибку в системе, которая дает сбой, не старайтесь имитировать проблему на другой системе. Не доверяйте статистике непостоянных проблем; они скорее скроют ошибку, чем проявят ее. Вместо этого постарайтесь сделать ее устойчивой, изменяя вводимые данные, начальные условия и координацию действий. Если ошибка все еще непостоянна, вам придется сделать так, чтобы она выглядела постоянной. При каждом запуске фиксируйте в журнале каждый бит информации, какой только сможете; затем, когда есть успешные запуски и сбои, сравните их друг с другом. Если вы собрали достаточно данных, вы сможете нацелиться на проблему, как если бы смогли вызывать ошибку все время. Способность вызывать каждый раз ошибку означает также, что вы сможете сказать, когда вы ее исиравили. 3. Прекратите думать и смотрите. Имеется больше способов появления ошибок, чем вы можете себе представить. Поэтому не представляйте, что могло бы случиться, смотрите на это - оснастите систему инструментарием, чтобы вы действительно смогли увидеть механизм ошибки. Используйте любой инструментарий, который можете - отладчики, printf (), assert (), анализаторы логики и даже светодиоды и звуковые сигнализаторы. Проверяйте достаточно глубоко, пока ошибка не станет очевидной для глаз, а не только для мозга. * http : / /www. debuggingrules . com - Примеч. автора. http: / /www. amacombooks . org - Примеч. автора. 20*
596 Глава 15. Отладка Если вы все же догадались, используйте догадку, чтобы сфокусировать поиск - не старайтесь исправить, пока вы ее не увидите. Если вам приходится добавлять код инструментария, сделайте это, но убедитесь, что начинаете с той же самой базы кода, как на проблемной системе, и убедитесь, что ошибка все еще возникает при работе вашего добавленного кода. Часто добавление отладчика вызывает исчезновение ошибки (вот почему его называют отладчиком). 4. Разделяй и властвуй. Каждый это знает. Вы делаете последовательное приближение - начинаете с одного конца, перескакиваете полпути, смотрите, с какой стороны ошибка, затем перескакиваете оставшиеся полпути в направлении ошибки. Бинарный поиск, вы оказываетесь так за несколько прыжков. Трудной частью является определение того, прошли вы ошибку или нет. Одной из полезных уловок является помещение в систему известных, простых данных, так чтобы можно было легче узнать мусор. Начните также с плохого конца и работайте по направлению к хорошему: если вы начнете с хорошего конца, имеется слишком много хороших путей для исследования. Известные ошибки исправляйте сразу, поскольку иногда две ошибки взаимодействуют (хотя вы могли бы поклясться, что они не должны этого делать), и последовательное приближение не работает с двумя целевыми значениями. 5. Кажый раз изменяйте лишь что-то одно. Если вы стараетесь усовершенствовать модуль обработки потоков и одновременно переходите на следующую версию операционной системы, не имеет значения, видите ли вы улучшение, ухудшение или отсутствие изменений - у вас не будет мыслей по поводу того, каким был результат ваших отдельных изменений. Взаимодействие нескольких изменений может быть непредсказуемым и сбивающим с толку. Не делайте этого. Изменяйте за один раз только что-то одно, чтобы вы могли поручиться, что любое отличие, которые вы видите, возникло из-за этого изменения. Если вы делаете изменение, а результата не видно, немедленно вернитесь обратно. Возможно, оно имело результат, который вы не увидели, и он может проявиться в сочетании с другими изменениями. Это относится к изменениям как в тестировании, так и в кодировании. 6. Сохраняйте контрольные журналы. Многое в эффективности приведенных выше правил зависит от сохранения хороших записей. Во всех отношениях тестирования и отладки записывайте, что вы делали, когда вы это делали, как вы это делали и что случилось в результате. По возможности делайте это в электронном виде, чтобы записи можно было послать по электронной почте и прикрепить к базе данных ошибок. Многие ключи можно найти в паттернах событий, которые были бы незамечены, если бы все они не записывались для просмотра и сравнения. Ключ может находиться также в деталях, о которых вы думали, что они неважны, поэтому запишите юс все. 7. Проверьте подключение. У каждого есть история о какой-нибудь проблеме, оказавшейся в том, что «это не было подключено». Иногда что-то оказывается буквально не подключенным, но для программного обеспечения «не подключено» может означать отсутствующий драйвер или старую версию кода, о которой вы думали, что заменили ее. Или плохое оборудование, когда вы клянетесь, что это проблема программного обеспечения. В одной истории инженеры-программисты и электронщики показывали паль-
15,8. Рекомендуемая литература 597 цами друг на друга, и никто не был прав: тестирующее устройство, которое они использовали, не соответствовало спецификации. Основной момент в том, что иногда вы ищете проблему внутри системы, тогда как на самом деле она вне системы, или лежит в основе системы, или в инициализации системы, или вы смотрите не на ту систему. Не следует также непременно доверять своим инструментам. Производители инструментов также являются инженерами; у них есть ошибки, и вы можете оказаться тем, кто их обнаружит. 8. Оцените свежим взглядом. Есть три причины попросить помощи при отладке. Первая причина в получении свежего взгляда - другие люди часто видят что-то лишь потому, что они не вовлечены в это так, как вы. Вторая причина заключается в получении экспертной оценки - они знают о системе больше, чем вы. Третья причина заключается в получении опыта- они видели это,раньше. Когда вы описываете ситуацию кому-либо, сообщите о симптомах, которые вы видели, а не о своих предположениях, почему это происходит так. Вы пришли к ним, потому что ваши предположения не ведут вас никуда - не тяните их в ту же колею, в которую попали сами. 9. Если вы не исправили это, это ие исправлено. Так вы думаете, это исправлено? Испытайте. Раз вы могли заставить ошибку повторяться постоянно, создайте ту же самую ситуацию и убедитесь, что ошибки нет. Не думайте, что все исправлено лишь потому, что проблема была очевидной. Может, она не была такой очевидной. Может, ваше исправление не было сделано правильно. Может, ваше исправление даже не находится в новом выпуске! Проверьте! Заставьте ошибку исчезнуть. Вы уверены, что именно ваш код исправил проблему? Или это произошло из-за изменения теста, или туда был внесен какой-то другой код? Когда вы видите, что ваше исправление работает, уберите его и заставьте ошибку появиться снова. Затем верните исправление на место и убедитесь, что ошибки нет. Этот шаг гарантирует, что именно ваше исправление решило проблему. Дополнительные сведения о книге Debugging и плакат с правилами отладки можно найти для свободной загрузки по адресу http: / /www. debuggingrules. com. 15.8. Рекомендуемая литература Следующие книги отличные, с множеством сведений как в отношении тестирования, так и отладки. Все, кроме первой, относятся к программированию вообще. Их все стоит прочесть. 1. Debugging, David J. Agans. AMACOM, New York, New York, USA 2003. ISBN: 0-8144- 7168-4. Настоятельно рекомендуем эту книгу. У нее легкий стиль, удивительное звучание, чтение - одно удовольствие! 2. Programming Pearls, 2nd edition, by Jon Louis Bentley. Addison-Wesley, Reading, Massachusetts, USA, 2000. ISBN: 0-201-65788-0. См. также веб-сайт этой книги29.
598 Глава 15. Отладка В главе 5 этой книги приведено хорошее обсуждение тестирования элементов и построения тестовой среды. 3. Literate Programming, by Donald E. Knuth. Center for the Study of Language and Information (CSLI), Stanford University, USA, 1992. ISBN: 0-9370-7380-6. Эта восхитительная книга содержит ряд статей Дональда Кнута по грамотному программированию (literateprogramming) - методике программирования, которую он изобрел и использовал для создания ТеХ и Metafont. Особый интерес представляет статья, озаглавленная «Ошибки ТеХ», которая описывает, как он разрабатывал и отлаживал ТеХ, включая его журнал всех найденных и исправленных ошибок. 4. Writing Solid Code, by Steve Maguire. Microsoft Press, Redmond, Washington, USA, 1993. ISBN: 1-55615-551-4. 5. Code Complete: A Practical Handbook of Software Construction, by Steve McConnell. Microsoft Press, Redmond, Washington, USA, 1994. ISBN: 1-55615-484-4. 6. The Practice of Programming, by Brian W. Kernighan and Rob Pike. Addison-Wesley, . Reading, Massachusetts, USA, 1999. ISBN: 0-201-61585-X. 15.9. Резюме Отладка является важной частью разработки программного обеспечения. Для минимизации ошибок должны использоваться хорошие проектирование и практика разработки, но отладка всегда будет с нами. Программы должны компилироваться без оптимизации и с включенными идентификаторами отладки, чтобы упростить отладку под отладчиком. На многих системах компиляция с оптимизацией и компиляция с идентификаторами отладки несовместимы. Это не относится к GCC, вот почему разработчикам GNU/Linux нужно знать об этой проблеме. Отладчик GNU GDB является стандартом на системах GNU/Linux и может использоваться также почти на любой коммерческой системе Unix. (Также доступны и легко переносимы графические отладчики на основе GDB.) Контрольные точки, отслеживаемые точки и пошаговое исполнение с посредством next, step и cont предоставляют базовый контроль над программой при ее работе. GDB позволяет также проверять данные и вызывать функции внутри отлаживаемой программы. Имеется множество вещей, которые вы можете сделать при написании программы для ее упрощения, когда неизбежно придется ее отлаживать. Мы рассмотрели следующие темы: • Отладочные макросы для вывода состояния. • Избегание макросов с выражениями. • Перестановку кода для облегчения пошагового выполнения. • Написание вспомогательных функций для использования их из отладчика. • Избегание объединений. http://www.cs.bell-labs.com/cm/cs/pearls/ - Примеч. автора.
Упражнения 599 • Помещение отладочного кода времени исполнения в готовую версию программы и обеспечение различных способов включения вывода этого кода. • Добавление фиктивных функций для упрощения установки контрольных точек. • Для помощи при отладке помимо простых отладчиков общего назначения существует ряд инструментов и библиотек. Библиотека dbug предоставляет элегантный внутренний отладчик, который использует многие из описанных нами методик последовательным, связанным образом. Существует множество отладочных библиотек для динамической памяти, имеющие сходные свойства. Мы рассмотрели три из них (mtrace, Electric Fence и dmalloc) и предоставили ссылки на несколько других. Программа Valgrind идет еще дальше, обнаруживая проблемы, относящиеся к неинициализированной памяти, а не только к динамической памяти. splint является современной альтернативой многоуважаемой программе V7 lint. Она доступна по крайней мере на системе одного из поставщиков GNU/Linux и легко может быть загружена и построена из исходных кодов. Помимо инструментов отладки, неотъемлемой частью процесса разработки программного обеспечения является также тестирование программ. Ее следует понять, запланировать и управлять ею с самого начала любого проекта разработки программного обеспечения, даже индивидуального. Отладка является умением, которому можно научиться. Мы рекомендуем прочесть книгу Debugging Дэвида Дж. Эганса и научиться применять его правила. Упражнения 1. Откомпилируйте одну из ваших программ с помощью GCC, используя как -д, так и -О. Запустите ее под GDB, установив контрольную точку в main(). Выполните программу пошагово и посмотрите, насколько близко соответствует (или не соответствует) исполнение оригинальному исходному коду. Это особенно хорошо делать с кодом, использующим циклы while или for. 2. Прочитайте об особенности GDB условной контрольной точки. Насколько это упрощает работу с проблемами, которые появляются лишь после того, как будет сделано определенное число операций? 3. Перепишите функцию parse_debug () из раздела 15.4.2.1 «Добавляйте отладочные опции и переменные», чтобы использовать таблицу строк опций отладки, значений флагов и длин строк. 4. (Трудное.) Изучите исходный код gawk; в частности, структуру NODE в awk. h. Напишите вспомогательную отладочную функцию, которая выводит содержимое NODE, основываясь на значении в поле type. 5. Возьмите одну из своих программ и измените ее так, чтобы использовать библиотеку dbug. Откомпилируйте ее сначала без -DDBUG, чтобы убедиться, что она компилируется и работает нормально. (Есть ли у вас для нее набор возвратных тестов? Прошла ли ваша программа все тесты?)
600 Глава 15. Отладка Убедившись, что добавление библиотеки dbug не нарушает работу вашей программы, перекомпилируйте ее с -DDBUG. По-прежнему ли проходит ваша программа все свои тесты? Какова разница в производительности при включенной и отключенной библиотеке? Запустите ваш тестовый набор с опцией -# t, чтобы увидеть трассировку вызовов функций. Как вы думаете, это поможет вам в будущем, когда придется иметь дело с отладкой? Почему да или почему нет? 6. Запустите одну из своих программ, использующих динамическую память, с Electric Fence или одним из других тестеров динамической памяти. Опишите проблемы, которые вы обнаружили, если они есть. 7. Перезапустите ту же самую программу, используя Valgrind с включенным режимом проверки утечек. Опишите найденные вами проблемы, если они есть. 8. Разработайте набор тестов для программы mv. (Прочтите mv(\)\ убедитесь, что охватили все ее опции.) .9. Поищите в Интернете ресурсы по тестированию программного.обеспечения. Какие интересные вещи вы нашли?
Глава 16 Проект, связывающий все воедино В этой главе: • 16.1. Описание проекта 601 • 16.2. Рекомендуемая литература 603 JK первой половине этой книги мы довольно аккуратно связали все, что было представлено, рассмотрев V7 Is . с. Однако, нет достаточно небольшой программы, насколько бы это нам хотелось, чтобы связать воедино все концепции и API, представленные начиная с главы 8 «Файловые системы и обходы каталогов». 16.1. Описание проекта В повседневном использовании единственной программой, которая действительно использует почти все в этой книге, является оболочка. И на самом деле есть книги по программированию на Unix, в которых пишется небольшая, но работающая оболочка для иллюстрации использованных принципов. Настоящие оболочки являются большими и беспорядочными творениями. Они должны иметь дело со многими проблемами переносимости, такими, которые мы обрисовывали по всей книге, а помимо этого, они часто должны обходить различные ошибки в различных версиях Unix. Более того, чтобы быть полезными, оболочки делают множество вещей, которые не затрагивают API системных вызовов, такие, как хранение переменных оболочки, историю сохраненных команд и т. д. Предоставление завершенного обзора полноценной оболочки, такой как Bash, ksh93 или zsh, потребовало бы отдельной книги. Вместо этого мы рекомендуем следующий список шагов по написанию своей собственной оболочки либо в качестве (большого) упражнения для закрепления вашего понимания, либо, возможно, в качестве совместного проекта, если вы обучаетесь в учебном заведении. 1. Спроектируйте свой командный «язык», чтобы его было легко интерпретировать с помощью простого кода. Хотя технология компиляторов и интерпретаторов полезна при написании оболочки как изделия, для вас на данный момент это, вероятно, излишне. Рассмотрите следующие моменты: • Собираетесь ли вы использовать возможности интернационализации? • Какие команды должны быть встроены в оболочку? • Чтобы быть полезной, в вашей оболочке должен быть механизм пути поиска команд, аналогичный $РАТН в обычной оболочке. Как вы его установите?
602 Глава 16. Проект, связывающий все воедино • Какие перенаправления ввода/вывода вы хотите поддержать? Только файлы? Также и каналы? Хотите ли вы иметь возможность перенаправлять нет только дескрипторы файлов 0, 1 и 2? • Решите, как будут работать кавычки: одинарные и двойные? Или лишь одна разновидность? Как вы поместите в кавычки сами кавычки? Как кавычки будут взаимодействовать с перенаправлениями ввода/вывода? • Как вы обработаете вызов команд в фоновом режиме? Что насчет ожидания завершения работы команды в фоновом режиме? • Решите, будут ли у вас переменные оболочки. • Какую разновидность символов подстановки или других расширений будете вы поддерживать? Как это взаимодействует с кавычками? С переменными оболочки? • Вы должны запланировать по крайней мере операторы if и while. Спроектируйте синтаксис. Мы будем называть их блочными операторами. • Решите, хотите ли вы разрешить перенаправления ввода/вывода для блочных операторов. Если да, как будет выглядеть синтаксис? • Решите, как язык вашей оболочки должен обрабатывать сигналы, если он вообще это делает. • Разработайте шаблон тестирования и отладки до того, как вы начнете программировать. 2. Если вы собираетесь использовать возможности интернационализации, делайте это с самого начала. Последующая ее вставка тяжела. 3. Для настоящей работы начните просто. Начальная версия должна читать одну строку за раз и разделять ее на слова для использования в качестве отдельных аргументов. Не используйте кавычек,, перенаправления ввода-вывода или что-нибудь еще. Не старайтесь даже создать новый процесс для запуска введенной программы. Как вы собираетесь тестировать то, что у вас пока есть? 4. Добавьте кавычки так, чтобы отдельные «слова» могли содержать разделители. Реализует ли код для кавычек ваш проект? 5. Заставьте работать ваши встроенные команды. (По крайней мере две нужные встроенные команды см. в разделах 4.6 «Создание файлов» и 8.4.1 «Смена каталога: chdir () и f chdir () ».) Как вы собираетесь их тестировать? 6. Первоначально используйте фиксированный путь поиска, такой как "/bin:/usr/ bin: /usr/local/bin". Добавьте создание процесса при помощи fork() и его исполнение при помощи exec (). (См. главу 9 «Управление процессами и каналы».) Запустив новую программу, оболочка должна ждать ее завершения. 7. Добавьте фоновое исполнение и, в качестве отдельной команды, ожидание завершения выполнения процесса (см. главу 9 «Управление процессами и каналы»). 8. Добавьте устанавливаемый пользователем путь поиска (см. раздел 2.4 «Переменные окружения»). 9. Добавьте перенаправление ввода/вывода для файлов (см. раздел 9.4 «Управление дескрипторами файлов»). 10. Добавьте переменные оболочки. Протестируйте их взаимодействие с кавычками. 11. Добавьте символы подстановки и другие расширения (ем. раздел 12.7 «Расширения метасимволов»). Протестируйте их взаимодействие с переменными оболочки. Протестируйте их взаимодействие с кавычками.
16.2. Рекомендуемая литература 603 12. Добавьте конвейеры (см. раздел 9.3 «Базовое межпроцессное взаимодействие: каналы и очереди FIFO»). С этого момента начинаются настоящие сложности. Вам может потребоваться тщательно рассмотреть то, как вы управляете данными, представляющими запускаемые команды. Здесь можно было бы остановиться с законным чувством достижения, если вы получили работающую оболочку, которая может делать все, упомянутое до сих пор. 13. Если вы принимаете дальнейший вызов, добавьте операторы if и/или while. 14. Добавьте обработку сигналов (см. главу 10 «Сигналы»). 15. Если вы хотели бы использовать свою оболочку для настоящей работы, изучите библиотеку GNU Readline (наберите в системе GNU/Linux 'info readline' или посмотрите исходный код для оболочки Bash). Эта библиотека дает вам возможность добавлять к интерактивным программам возможность редактирования командной строки в стиле Emacs или vi. Постоянно держите в уме две вещи: всегда имейте возможность протестировать то, что вы делаете; и «никаких произвольных ограничений»! Когда все это сделано, проделайте анализ сделанного проекта. Как вы сделали бы его по-другому во второй раз? Удачи! 16.2. Рекомендуемая литература 1. The UNIX Programming Environment, by Brian W. Kemighan and Rob Pike. Prentice-Hall, Englewood Cliffs, New Jersey, USA, 1984. ISBN: 0-13-937699-2l. Эта классическая книга по программированию на Unix, описывающая целостную структуру окружения Unix, от интерактивного использования до программирования оболочки, программирования с помощью функций <stdio.h> и низкоуровневых системных вызовов, разработки программ с помощью make, yacc и lex, и документирования с помощью nrof f и trof f. Хотя возраст книги приличный, ее в высшей степени стоит прочесть, и мы ее чрезвычайно рекомендуем. 2. The Art of UNIX Programming, by Eric S. Raymond. Addison-Wesley, Reading, Massachusetts, USA, 2004. ISBN: 0-13-142901-9. Это книга на более высоком уровне фокусируется на проблемах проектирования при программировании в Unix: как работают программы Unix и как разрабатывать свои собственные программы, чтобы уютно вписаться в окружение Linux/Unix. Хотя мы не всегда согласны со многим из того, что хочет сказать автор, книга действительно содержит значительное количество важного материала, и ее стоит прочесть. Русский перевод: Брайн Керниган, Роб Пайк. UNIX. Программное окружение. Санкт-Петербург, Символ- Плюс, 2003. - Примеч. науч. ред.
Часть 4 Приложения Приложение А. Научитесь программированию за десять лет ------ 605 Приложение В. Старая лицензия Unix Caldera - - 610 Приложение С. Общедоступная лицензия GNU 612
Приложение А Научитесь программированию за десять лет «Опыт, сущ.: Нечто, что вы не получаете до тех пор, пока это вам не понадобится». -Оливер- ^Данная глава написана Петером Норвигом (Peter Norvig, © 2001 г.). Воспроизводится по разрешению. Оригинальную статью, включая гиперссылки, можно найти по адресу http://www.norvig.com/21~days.html. Мы включили ее, поскольку полагаем, что она содержит важную идею. Приведенная цитата является одной из наших давних любимых, и поскольку она применима к сути нашего приложения, мы ее также включили. Почему каждый в такой спешке? Зайдите в любой книжный магазин, и вы увидите «Научитесь Java за 7 дней» наряду с бесконечными вариациями, предлагающими научиться Visual Basic, Windows, Internet и т. д. за несколько дней или часов. Я произвел следующий расширенный поиск на Amazon. com: pubdate: after 1992 and title: days and (title: learn or title: teach yourself) и получил 248 попаданий. Первые 78 были компьютерными книгами (номер 79 был «Изучите Бенгали за 30 дней»). Я заменил «дни» ('days') на «часы» ('hours') и получил замечательным образом сходные результаты: еще 253 книг, из них 77 компьютерных, за которыми следовала под номером 78 «Научитесь грамматике и стилистике за 24 часа». Всего из верхних 200 книг 96% были компьютерные. Вывод таков, что либо люди очень торопятся изучить компьютеры, либо эти компьютеры каким-то образом фантастически легче изучить, чем что-то еще. Книг о том, как изучить Бетховена, или квантовую физику, или даже научиться ухаживать за собакой за несколько дней, нет. Давайте проанализируем, что может означать название наподобие «Изучите Паскаль за три дня»: Изучите: за 3 дня у вас не будет времени написать несколько значительных программ и научиться из них на своих успехах и неудачах. У вас не будет времени
606 Приложение А. Научитесь программированию за десять лет поработать с опытным программистом и понять, на что похожа жизнь в этом окружении. Короче, у вас не будет времени научиться многому. Поэтому можно говорить лишь о поверхностном знакомстве, а не глубоком понимании. Как сказал римский папа Александр, неглубокое знание является опасной вещью. • Паскаль: за 3 дня вы смогли бы изучить синтаксис Паскаля (если вы уже знаете сходный язык), но не смогли бы много узнать о том, как использовать этот синтаксис. Короче, если бы вы были программистом на Бейсике, вы смогли бы писать программы в стиле Бейсика на Паскале, но не смогли бы изучить, для чего Паскаль в действительности подходит (или не подходит). В чем же суть? Алан Перлис (Alan Perlis) сказал однажды: «Язык, который не влияет на способ вашего мышления о программировании, не стоит того, чтобы его знали». Другими словами, вам приходится изучить крошечный кусок Паскаля (или, более вероятно, чего-то наподобие Visual Basic или JavaScript), поскольку вам нужно взаимодействовать с существующим инструментом для выполнения определенной задачи. Но тогда вы не обучаетесь программированию; вы учитесь выполнять эту задачу. За три дня: к сожалению, этого недостаточно, как показывает следующий раздел. Научитесь программированию за десять лет Ученые (Hayes, Bloom) показали, что развитие высокой квалификации в любой из широкого разнообразия областей, включая шахматы, сочинение музыки, рисование, игра на фортепьяно, плавание, теннис, исследования в нейропсихологии и топологии, занимают около десяти лет. По-видимому, в действительности не может быть сокращения: даже Моцарту, который был музыкально одаренным уже в 4 года, потребовалось еще 13 лет, прежде чем он начал создавать мировую классическую музыку. В другом жанре Битлз, казалось, вырвались на сцену, появившись в шоу Эда Салливана в 1964 г. Но они играли с 1957 года, и хотя они рано завоевали широкую популярность, их первый переломный успех, Сержант Пепперс, был выпущен в 1967 г. Сэмюэл Джонсон (Samuel Johnson) считал, что требуется более десяти лет: «Мастерство в любой отрасли достигается лишь работой в течение жизни; его нельзя купить по меньшей цене». А Чосер (Chaucer) жаловался: «Жизнь так коротка, а ремеслу так долго учиться». Вот мой рецепт для успеха в программировании: Заинтересуйтесь программированием и сделайте что-нибудь, потому что это забавно. Убедитесь, что оно продолжает оставаться достаточно интересным, чтобы вы хотели прилагать усилия в течение десяти лет. Говорите с другими программистами; читайте другие программы. Это важнее, чем любая книга или учебный курс. Программируйте. Лучшей разновидностью обучения является обучение деланием. Говоря более технически, «максимальный уровень производительности индивидуума в данной области не достигается автоматически как функция расширения опыта, но его могут повысить даже очень опытные индивидуумы в результате обдуманных усилий по совершенствованию» и «наиболее эффективное обучение требует хорошо определенной задачи с соответствующим уровнем трудности для данных конкретных
Научитесь программированию за десять лет 607 индивидуума, информационной обратной связи и возможностей повторения и исправления ошибок». Книга Cognition in Practice: Mind, Mathematics, and Culture in Everyday Life (Практическое познание: мышление, математика и совершенствование способностей в повседневной жизни) является интересным справочным пособием для этой точки зрения. Если хотите, проведите четыре года в колледже (или еще больше в аспирантуре). Это даст вам доступ к некоторым видам работ, требующим диплома, и это даст более глубокое понимание области, но если вам не нравится школа, вы можете (с некоторым упорством) получить аналогичный опыт на работе. В любом случае, одного лишь изучения книг недостаточно. «Образование в компьютерных науках может сделать кого-нибудь искусным программистом не в большей степени, чем изучение кистей и красок может сделать кого-то искусным художником», - говорит Эрик Реймонд (Eric Raymond), автор The New Hacker's Dictionary (Словаря новых хакеров). Один из лучших программистов, которых я когда-либо принимал на работу, имел лишь среднее образование; он создал массу превосходных программ, у него есть своя группа новостей и через фондовые опционы, без сомнения, намного богаче, чем буду я когда-либо. Работайте над проектами с другими программистами. Будьте лучшим программистом в некоторых проектах; будьте худшим в некоторых других. Когда вы лучший, вы принимаетесь проверять свои способности возглавлять проект и вдохновлять других своим видением. Когда вы худший, вы изучаете то, что делают мастера, и вы изучаете, что они не любят делать (поскольку они заставляют делать это за себя вас). • Работайте над проектами после других программистов. Погрузитесь в понимание программы, написанной кем-то еще. Посмотрите, чего стоит понять и исправить ее^ когда рядом нет авторов программы. Подумайте над тем, как спроектировать свои программы, чтобы сделать их проще для тех, кто будет их сопровождать после вас. • Изучите по крайней мере полдюжины языков программирования. Включите один язык, поддерживающий абстракции классов (подобно Java или C++), один, поддерживающий функциональные абстракции (подобно Lisp или ML), один, поддерживающий синтаксические абстракции (подобно Lisp), один, поддерживающий декларативные спецификации (подобно Prolog или шаблонам C++), один, поддерживающий сопрограммы (подобно Icon или Scheme), и одни, поддерживающий параллелизм (подобно Sisal). Помните, что в «компьютерных науках» есть «компьютер». Знайте, сколько времени ваш компьютер тратит на исполнение инструкции, получение слова из памяти (с попаданием в кэш и без попадания), чтение последовательных слов с диска и поиск нового положения на диске. (Ответы ниже.) Погрузитесь в работу по стандартизации языка. Это может быть комитет ANSI C++, или это может быть принятием решения, должен ли ваш местный стиль программирования использовать 2 или 4 пробела в отступах. В любом случае, вы узнаете, что любят в языке другие люди, насколько глубоко они это чувствуют и, возможно, даже немного о том, почему они это чувствуют. • Имейте здравый смысл, чтобы отделаться от работы по стандартизации языка как можно скорее. Держа все это в уме, сомнительно, насколько далеко вы сможете уйти, обучаясь лишь по книгам. До рождения моего Первого ребенка я прочел все книги How To (Как...), и до сих пор чувствую себя необразованным новичком. 30 месяцев спустя, когда ожидался мой
608 Приложение А. Научитесь программированию за десять лет второй ребенок, вернулся ли я к книгам, чтобы освежить их в памяти? Нет. Вместо этого я полагался на свой собственный опыт, который оказался для меня намного более полезным и обнадеживающим, чем тысячи страниц, написанных экспертами. Фред Брукс (Fred Brooks) в своем эссе Никаких серебряных пуль (No Silver Bullets) определил план из трех частей для обнаружения великих проектировщиков программного обеспечения: 1. Систематически как можно раньше распознавать ведущих проектировщиков. 2. Назначить наставников по достижениям, ответственных за разработку перспективы и тщательно хранить архивы достижений. 3. Предоставлять растущим проектировщикам возможности для взаимодействия и стимулирования ими друг друга. Это предполагает, что у некоторых людей уже есть качества, необходимые, чтобы стать великими проектировщиками; задача заключается в том, чтобы соответствующим образом их выманить. Алан Перлис (Alan Pedis) выразился более лаконично: «Каждого можно научить ваять: Микеланджело пришлось бы учить, как не делать это. Так же и с великими программистами». Поэтому вперед, купите эту книгу по Java; возможно, вы получите от нее какую- нибудь пользу. Но вы не измените свою жизнь или свою действительную общую квалификацию как программиста за 24 часа, дня или даже месяцев. Ссылки Bloom, Benjamin (ed.) Developing Talent in Young People, Ballantine, 1985. Brooks, Fred, No Silver Bullets, IEEE Computer, vol. 20, no. 4, 1987, p. 10-19. Hayes, John R., Complete Problem Solver, Lawrence Erlbaum, 1989. Lave, Jean, Cognition in Practice: Mind, Mathematics, and Culture in Everyday Life, Cambridge University Press, 1988. Ответы Время выполнения различных операций на типичном ПК 1 ГГц летом 2001 г.: исполнение одной инструкции 1 не = A/1000 000 000) сек выборка слова из кэша L1 2 нс выборка слова из основной памяти 10 не выборка смежного слова с диска 200 не выборка слова из нового места на диске (поиск) 8 000 000 не = 8 мс
Сноски 609 Сноски Эта страница1 доступна также в переводе на японский язык2 благодаря Yasushi Mu- rakawa и в переводе на испанский язык3 благодаря Carlos Rueda. Т. Capey указывает, что страница Complete Problem Solver на Amazon теперь содержит книги Teach Yourself Bengali in 21 days и Teach Yourself Grammar and Style под рубрикой «Клиенты, которые купили эту книгу, купили также и эти книги». Я догадываюсь, что большая часть людей, посмотревших на ту книгу, пришли с этой страницы. 1 Это приложение приведено в буквальном виде с веб-страницы, указанной вначале. - Примеч. автора. http: //wwwl .neweb.ne. jp/wa/yamdas/column/technique/21-daysj .html-Примеч. автора. http: //loro . sf .net/notes/21 -dias .html -Примеч. автора.
Приложение В Лицензия Caldera для старой Unix3 а. Это - неофициальный перевод Лицензии Caldera для старой Unix на русский язык. Он не был опубликован Caldera International, Inc. и не может легально определять условия распространения программных продуктов, использующих Лицензию Caldera - только оригинальный английский текст Лицензии Caldera для старой Unix имеет законную силу. CALDERA 240 West Center Street Orem, Utah 84057 801-765-4999 Fax 801-765-4481 23 января 2002 г. Дорогие энтузиасты UNIX ®, . • Caldera International, Inc. настоящим предоставляет безвозмездную лицензию, которая включает права на использование, модификацию и распространение этого названного исходного кода, включая создание производных двоичных изделий из исходного кода. Исходный код, для которого Caldera International, Inc. передает права, ограничены следующими операционными системами UNIX, которые работают на 16-разрядном процессоре PDP-11 и ранних версиях 32-разрядной операционной системы UNIX, со специальным исключением UNIX System III и UNIX System V и операционных систем-наследников: 32-разрядной 32V UNIX 16-разрядной UNIX версий 1, 2, 3, 4, 5, 6, 7 Caldera International, Inc. не дает никаких гарантий или поручительств, что какой- нибудь исходный код доступен от Caldera International, Inc. Следующее уведомление об авторских правах применяется к файлам исходного кода, для которых предоставляется данная лицензия. Copyright (С) Caldera International Inc. 2001 - 2002. Все права сохранены. Разрешается распространение и использование в исходной и двоичной форме, с модификациями или без них, при соблюдении следующих условий: При распространении исходного кода и документации должно быть сохранено вышеприведенное уведомление об авторских правах, данный список условий и следующий отказ от ответственности. При распространении в двоичном виде вышеприведенное уведомление об авторских правах, данный список условий и следующий отказ от ответственности должны быть воспроизведены в документации и/или в других материалах, предоставляемых при распространении.
611 Все рекламные материалы, упоминающие особенности или использование данного программного обеспечения, должны отображать следующее признание: Этот продукт включает программное обеспечение, разработанное или принадлежащее Caldera International, Inc. Ни название Caldera Internatiorial, Inc., ни названия других внесших вклад участников не могут использоваться для поддержки или продвижения продуктов, производных от данного программного обеспечения, без особого предварительного письменного разрешения. ИСПОЛЬЗОВАНИЕ ЭТОГО ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ПРЕДУСМОТРЕНО ПО ЛИЦЕНЗИИ CALDERA INTERNATIONAL, INC. И ДРУГИХ ВНЕСШИХ ВКЛАД УЧАСТНИКОВ «КАК ЕСТЬ» И БЕЗ ВСЯКИХ ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ ГАРАНТИЙ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ НЕЯВНЫМИ ГАРАНТИЯМИ ПРИГОДНОСТИ ДЛЯ ПРОДАЖИ ИЛИ ПРИМЕНИМОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. НИ В КОЕМ СЛУЧАЕ CALDERA INTERNATIONAL, INC. HE НЕСЕТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБОЙ ПРЯМОЙ, КОСВЕННЫЙ, СЛУЧАЙНЫЙ, СПЕЦИАЛЬНЫЙ, ШТРАФНОЙ ИЛИ ЯВЛЯЮЩИЙСЯ СЛЕДСТВИЕМ УЩЕРБ (ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ПРИОБРЕТЕНИЕМ ИЛИ ЗАМЕНОЙ ТОВАРОВ; ПОТЕРЮ ЦЕННОСТИ, ДАННЫХ, УПУЩЕННУЮ ВЫГОДУ ИЛИ ПРИОСТАНОВКУ БИЗНЕСА), КАК БЫ ОН НИ БЫЛ ВЫЗВАН И В СООТВЕТСТВИИ С КАКИМИ БЫ ТО НИ БЫЛО ПРЕДПОЛОЖЕНИЯМИ, БУДЬ ТО В КОНТРАКТЕ, НЕПОСРЕДСТВЕННОЙ ОТВЕТСТВЕННОСТИ ИЛИ ГРАЖДАНСКОМ ПРАВОНАРУШЕНИИ (ВКЛЮЧАЯ НЕБРЕЖНОСТЬ ИЛИ ДРУГОЕ), ВОЗНИКШИЕ ЛЮБЫМ СПОСОБОМ ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ ДАННОГО ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ, ДАЖЕ В СЛУЧАЕ ПРЕДУПРЕЖДЕНИЯ О ВОЗМОЖНОСТИ ТАКОГО УЩЕРБА. Искренне ваш, (подпись) Bill Broderick Bill Broderick Директор, Служба лицензирования UNIX является зарегистрированной торговой маркой Open Group в США и других странах.
Приложение С Общедоступная лицензия GNUa а. Это - неофициальный перевод Общедоступной лицензии GNU на русский язык. Он не был опубликован Фондом Свободного Программного Обеспечения и не может легально определять условия распространения программных продуктов, использующих Общедоступную лицензию GNU - только оригинальный английский текст Общедоступной лицензии GNU имеет законную силу. Версия 2, июнь 1991 г. Copyright © 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111, USA x азрешается копировать и распространять дословные копии данного лицензионного документа, но не разрешается его изменять. Преамбула Лицензии большинства программных продуктов составлены так, чтобы отобрать у вас право совместно использовать и изменять продукт. По контрасту, Общедоступная лицензия GNU (GNU General Public License), напротив, подразумевает вашу свободу в совместном использовании и изменении свободного программного обеспечения - чтобы гарантировать, что программное обеспечение является свободным для всех своих пользователей. Данная'Общедоступная лицензия применяется к большей части программного обеспечения Фонда независимых программ (Free Software Foundation) и к любой другой программе, авторы которой передают ее на использование с данной лицензией. (Некоторое другое программное обеспечение Фонда независимых программ защищается вместо этого Общедоступной библиотечной лицензией GNU (GNU Library General Public License).) Вы также можете использовать ее для своих программ. Когда мы говорим о независимом программном обеспечении, мы имеем в виду свободу, а не цену. Наши Общедоступные лицензии разработаны так, чтобы гарантировать вам свободу распространять копии независимого программного обеспечения (и назначать за это цену, если хотите), получать исходный код, если хотите, изменять программное обеспечение или его части в новых независимых программах; а также гарантировать, что вы будете знать обо всех этих вещах. Чтобы защитить ваши права, нам необходимо предусмотреть ограничения, которые запрещают кому бы то ни было отказывать вам в этих правах или просить вас от них отка-
Права и обязанности при копировании, распространении и модификации 613 заться. Эти ограничения преобразуются в определенные ваши обязанности, если вы распространяете копии программного обеспечения или модифицируете его. Например, если вы распространяете копии таких программ, вне зависимости от того, бесплатно или за плату, вы обязаны дать получателям все права, которые есть у вас. Вы должны также обеспечить для них получение или возможность получения исходного кода. И вы должны довести до их сведения эти положения, чтобы они знали о своих правах. Мы защищаем ваши права в два этапа: A) обеспечивая авторское право на программное обеспечение и B) предоставляя вам эту лицензию, которая дает вам законное разрешение копировать, распространять и/или модифицировать программное обеспечение. Также, для защиты каждого автора и нашей собственной, мы хотим удостовериться в том, что каждый понимает, что на это независимое программное обеспечение нет гарантий. Если программное обеспечение модифицируется кем-либо еще и распространяется дальше, мы хотим, чтобы его получатели знали, что то, что они получают, не является оригиналом, поэтому любые проблемы, привнесенные другими, не должны отражаться на репутации автора оригинального продукта. Наконец, любая независимая программа постоянно подвергается угрозе со стороны патентов на программное обеспечение. Мы хотим избежать опасности, когда распространители независимой программы получают отдельные патентные права, фактически превращая программу в патентованную. Чтобы предотвратить это, мы ясно дали понять, что любой патент должен предусматривать свободное его использование всеми или не регистрироваться вообще. Далее следуют точные права и обязанности при копировании, распространении и модификации. Права и обязанности при копировании, распространении и модификации 0. Данная лицензия распространяется на любую программу или другой продукт, который содержит размещенное владельцем авторских прав уведомление, заявляющее, что этот продукт может распространяться в соответствии с условиями данной Общедоступной лицензии. Используемый далее термин «Программа» относится к любым таким программе или продукту, а «продукт, основанный на Программе» означает либо саму Программу, либо любой производный продукт, защищаемый законом об авторском праве: то есть продукт, содержащий Программу или часть ее, воспроизведенный точно или с модификациями и/или переведенный на другой язык. (В дальнейшем перевод без ограничений включается в термин «модификация».) К каждому обладателю лицензии обращение следует как «вы». Другая деятельность, отличная от копирования, распространения и модификации, не охватывается данной Лицензией; она выходит за рамки ее действия. Акт запуска Программы на выполнение не ограничивается, а результаты работы Программы попадают
614 / Приложение С. Общедоступная лицензия GNU под действие Лицензии лишь в том случае, если их содержание составляет продукт на основе Программы (независимо от того, было ли это сделано путем запуска Программы). Правомочность этого положения зависит оттого, что делает Программа. 1. Вы можете копировать и распространять дословные копии исходного кода Программы в том виде, как вы его получили, с помощью любого средства при условии, что вы открыто и соответствующим образом опубликуете в каждой копии соответстзуюидее заявление об авторских правах и отказе от гарантий; сохраните без изменений все заявления, ссылающиеся на данную Лицензию и на отсутствие каких-либо гарантий; и предоставите всем остальным получателям Программы вместе с самой Программой копию данной Лицензии. Вы можете назначить плату за акт физического копирования, вы можете также по своему усмотрению предоставить гарантию на платной основе. 2. Вы вправе модифицировать свою копию или копии Программы или любую их часть, создавая тем самым продукт, основанный на Программе, и копировать и распространять такие модификации или продукты на условиях Параграфа 1, приведенного выше, при условии, что вы будете также следовать всем нижеприведенным требованиям: a. Модифицированные файлы должны иметь заметные предупреждения о том, что вы изменили файлы, с указанием даты изменений. b. Любой продукт, который вы распространяете или публикуете, который целиком или частично является производным от Программы или любой ее части, должен быть целиком бесплатно лицензирован для всех третьих сторон в соответствии с условиями данной Лицензии. c. Если модифицированная программа обычно интерактивно принимает команды при работе, вы должны обеспечить при ее запуске стандартным способом для такого интерактивного использования выведение на печать или отображение на экране сообщения, включающего соответствующее заявление об авторских правах и заявление об отсутствии гарантий (либо, в противном случае, заявляющее о предоставлении гарантии вами) и о том, что пользователи могут далее распространять программу на данных условиях, и сообщающее пользователю, как можно просмотреть копию данной Лицензии. (Исключение: если сама Программа интерактивная, но обычно не выводит подобного сообщения, от вашего продукта, основанного на Программе, не требуется выводить это сообщение). Эти требования применяются к модифицированному продукту в целом. Если идентифицируемые разделы этого продукта не являются производными от Программы и могут с основанием рассматриваться независимыми и отдельными продуктами сами по себе, тогда данная лицензия и ее условия неприменимы к этим разделам при распространении их в качестве отдельных продуктов. Но когда вы распространяете те же разделы в виде целого, являющегося продуктом, основанным на Программе, распространение этого целого должно соответствовать положениям данной Лицензии, положения которой для обладателей лицензии распространяется на все целое и, таким образом, на каждую отдельную часть независимо от того, кто ее написал.
Права и обязанности при копировании, распространении и модификации 615 Таким образом, цель данного раздела состоит не в том, чтобы заявить свои права или оспорить ваши права на продукт, написанный исключительно вами; целью скорее является использование права контролировать распространение производных или совместных продуктов, основанных на Программе. В дополнение, простое объединение другого продукта, не основанного на Программе, с Программой (или продуктом, основанным на Программе) на одном носителе информации или среде передачи данных не включает другой продукт в сферу применения данной Лицензии. 3. Вы вправе копировать и распространять Программу (или продукт, основанный на ней, в соответствии с Параграфом 2) в виде объектного кода или исполняемой программы, при выполнении условий, оговоренных в Параграфах 1 и 2, обеспечив также соблюдение одного из следующих требований: a. Сопроводив ее соответствующим полным исходным кодом в электронной форме, который должен распространяться в соответствии с вышеприведенными условиями Параграфов 1 и 2, на носителях, обычно используемых для обмена программами; или b. Сопроводив ее письменным предложением, действительным по крайней мере в течение трех лет, на предоставление любой третьей стороне по цене, не превышающей стоимость затрат на физическое распространение исходного кода, полной копии соответствующего исходного кода в электронном виде для распространения в соответствии с вышеприведенными условиями Параграфов 1 и 2, на носителях, обычно используемых для обмена программами; или c. Сопроводив ее сведениями, которые вы получили в качестве предложения распространения соответствующего исходного кода. (Данный вариант допустим лишь для некоммерческого распространения и лишь в том случае, если вы получили программу в виде объектного кода или исполняемого модуля с данным предложением, в соответствии с вышеприведенным Подпунктом Ь.) Исходный код для продукта означает предпочтительную форму продукта для внесения изменений. Для продукта исполняемого модуля полный исходный код означает весь исходный код для всех модулей, который в нем содержится, плюс любые связанные с ним файлы определения интерфейсов, плюс сценарии, используемые для управления компиляцией и установки исполняемого файла. Однако, в виде особого исключения, распространяемый исходный код не нуждается во включении чего-либо, что обычно распространяется (либо в виде исходного кода, либо в бинарной форме) с главными компонентами (компилятором, ядром и т.д.) операционной системы, на которой исполняемый модуль запускается, если только этот компонент сам не сопровождает исполняемый модуль. Если распространение исполняемого или объектного кода осуществляется путем пред- ' ложения доступа к копированию из определенного места, в таком случае предложение эквивалентного доступа для копирования исходного кода из того же источника рассматривается как распространение исходного кода, даже если третья сторона не принуждается к копированию исходного кода вместе с объектным кодом.
616 Приложение С. Общедоступная лицензия GNU 4. Вы не вправе копировать, изменять, выдавать сублицензии или распространять Программу на иных условиях, чем предусмотрено настоящей Лицензией. Любая попытка копировать, изменять, выдавать сублицензии или распространять Программу иным способом является не имеющей юридической силы и ведет к автоматическому прекращению ваших прав, предусмотренных данной Лицензией. Однако стороны, получившие от вас копии или права на условиях данной Лицензии, сохранят свои лицензии до тех пор, пока эти стороны будут полностью соблюдать условия данной Лицензии. 5. От вас не требуется признавать данную Лицензию, поскольку вы не подписывали ее. Однако, ничто иное не дает вам прав на изменение или распространение Программы или производных от нее продуктов. Подобные действия запрещены законом, если вы не признаете данную Лицензию. Следовательно, фактом изменения или распространения Программы (или любого продукта на основе Программы) вы заявляете о своем признании данной Лицензии и всех содержащихся в ней требований и условий по копированию, распространению или изменению Программы или продуктов, основанных на ней. 6. Каждый раз при распространении Программы (или любого продукта, основанного на Программе), получатель автоматически становится обладателем лицензии от оригинального владельца авторских прав на копирование, распространение или изменение Программы в соответствии с данными требованиями и условиями. Вы не вправе налагать дополнительные ограничения на реализации предоставляемых здесь прав получателя. Вы не несете ответственности за обеспечение соблюдения третьей стороной данной Лицензии. 7. Если в результате судебного разбирательства или заявления о нарушении патентных прав, или по любой иной причине (не ограничиваясь вопросами патентов) на вас наложены ограничения (по предписанию суда, по соглашению или иные), которые противоречат условиям настоящей Лицензии, они не освобождают вас от условий данной Лицензии. Если вы не можете распространять Программу таким образом, чтобы одновременно выполнять свои обязательства в соответствии с настоящей Лицензией и с любыми другими относящимися к делу обязательствами, вы, как следствие, вовсе лишаетесь права распространять Программу. Например, если патентная лицензия не допускает безвозмездное распространение Программы всеми теми, кто прямо или косвенно получит от вас копии, единственным способом, с помощью которого вы можете удовлетворить и патентную лицензию, и данную Лицензию, будет полное воздержание от распространения Программы. Если любая часть данного параграфа считается недействительной или не могущей быть выполненной в каких-либо конкретных обстоятельствах, должна применяться оставшаяся часть параграфа, а параграф в целом должен применяться в других условиях. Целью данного параграфа не является склонить вас к нарушению каких-либо заявлений о патентных или иных правах собственности или оспорить любые такие заявления; единственная цель данного параграфа заключается в защите целостности системы распространения свободного программного обеспечения, которая реализуется практикой общедоступной лицензии. Многие люди сделали щедрый вклад в широкий спектр программного обеспечения, распространяемого посредством этой системы, в надежде на последовательное применение этой системы; делом автора/дарителя является решение, нужно ли распространять программу через любую другую систему, а обладатель лицензии не может повлиять на этот выбор.
Отказ от гарантий 617 Этот параграф нацелен на прояснение того, что подразумевается выводом оставшейся части настоящей Лицензии. 8. Если распространение и/или использование Программы в определенных странах ограничено либо патентами, либо охраняемыми авторским правом интерфейсами, оригинальный владелец авторских прав, помещающий Программу под сферу влияния данной Лицензии, может добавить явные географические ограничения на распространение, исключающие эти страны, таким образом, чтобы распространение было разрешено лишь в не исключенных странах. В таком случае, данная Лицензия включает в себя это ограничение, как если бы оно было написано в тексте самой данной Лицензии. 9. Фонд свободного программного обеспечения время от времени может публиковать пересмотренные и/или новые версии Общедоступной Лицензии. Такие новые версии будут сходны по духу с настоящей версией, но могут отличаться в деталях для учета новых проблем или интересов. Каждая версия получает отличительный номер версии. Если в Программе указан номер версии данной Лицензии, которая применяется к ней, и «любая последующая версия», вы имеете возможность соблюдать условия либо данной версии, либо любой последующей версии, опубликованной Фондом свободного программного обеспечения. Если в Программе не указан номер версии данной Лицензии, вы вправе выбрать любую версию, когда-либо публиковавшуюся Фондом свободного программного обеспечения. 10. Если вы хотите включить части Программы в другие свободные программы с иными условиями их распространения, напишите автору с просьбой о разрешении. Для программного обеспечения, авторским правом на которое обладает Фонд свободного программного обеспечения, напишите в Фонд свободного программного обеспечения; для этого мы иногда делаем исключения. Наше решение будет определяться двумя целями: сохранение свободного статуса всех производных от нашего свободного программного обеспечения и продвижение совместного и повторного использования программного обеспечения вообще. Отказ от гарантий 11. ПОСКОЛЬКУ ДАННАЯ ПРОГРАММА ЛИЦЕНЗИРУЕТСЯ БЕСПЛАТНО, ДЛЯ НЕЕ НЕ ПРЕДОСТАВЛЯЕТСЯ НИКАКИХ ГАРАНТИЙ, В ТОЙ СТЕПЕНИ, НАСКОЛЬКО ЭТО ПРИМЕНИМО ПО ЗАКОНУ. ЕСЛИ ПИСЬМЕННО НЕ УКАЗАНО ИНОЕ, ВЛАДЕЛЬЦЫ АВТОРСКОГО ПРАВА И/ИЛИ ДРУГИЕ СТОРОНЫ ПРЕДОСТАВЛЯЮТ ПРОГРАММУ «КАК ЕСТЬ», БЕЗ ГАРАНТИЙ КАКОГО-ЛИБО РОДА, ВЫРАЖЕННЫХ ЯВНО ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ТОЛЬКО ЭТИМ, ПОДРАЗУМЕВАЕМЫМИ ГАРАНТИЯМИ ПОЛЕЗНОСТИ И ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. ВЕСЬ РИСК, КАСАЮЩИЙСЯ КАЧЕСТВА И ПРОИЗВОДИТЕЛЬНОСТИ ПРОГРАММЫ, ЛОЖИТСЯ НА ВАС. ЕСЛИ ПРОГРАММА ОКАЖЕТСЯ С НЕДОСТАТКАМИ, ВЫ БЕРЕТЕ НА СЕБЯ РАСХОДЫ ПО ВСЕМУ НЕОБХОДИМОМУ ОБСЛУЖИВАНИЮ, ВОССТАНОВЛЕНИЮ ИЛИ ИСПРАВЛЕНИЮ.
618 Приложение С. Общедоступная лицензия GNU 12. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ, ЕСЛИ ТОЛЬКО ЭТО НЕ ТРЕБУЕТСЯ ПО СООТВЕТСТВУЮЩЕМУ ЗАКОНУ ИЛИ НЕ ОГОВОРЕНО ПО ПИСЬМЕННОМУ СОГЛАШЕНИЮ, НИ ОДИН ИЗ ВЛАДЕЛЬЦЕВ АВТОРСКИХ ПРАВ ИЛИ ЛЮБАЯ ДРУГАЯ СТОРОНА, КОТОРАЯ МОЖЕТ МОДИФИЦИРОВАТЬ И/ИЛИ РАСПРОСТРАНИТЬ ПРОГРАММУ НА ОГОВОРЕННЫХ ВЫШЕ УСЛОВИЯХ,.НЕ БУДЕТ ОТВЕЧАТЬ ЗА ПРИЧИНЕННЫЙ ВАМ УЩЕРБ, ВКЛЮЧАЯ ЛЮБОЙ ОБЩИЙ, ОСОБЫЙ, СЛУЧАЙНЫЙ ИЛИ КОСВЕННЫЙ УЩЕРБ, ПОНЕСЕННЫЙ ОТ ИСПОЛЬЗОВАНИЯ ИЛИ НЕВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ПРОГРАММЫ (ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ТОЛЬКО ЭТИМ, ПОТЕРИ ДАННЫХ ИЛИ НЕТОЧНЫЕ ОБРАБОТКИ ДАННЫХ ИЛИ ПОТЕРИ, КОТОРЫЕ ПОНЕСЛИ ВЫ ИЛИ ТРЕТЬИ ЛИЦА; ИЛИ НЕСПОСОБНОСТЬ ПРОГРАММЫ РАБОТАТЬ С ЛЮБЫМИ ДРУГИМИ ПРОГРАММАМИ), ДАЖЕ ЕСЛИ ВЛАДЕЛЕЦ АВТОРСКИХ ПРАВ ИЛИ ИНАЯ СТОРОНА БЫЛИ ПРЕДУПРЕЖДЕНЫ О ВОЗМОЖНОСТИ ТАКОГО УЩЕРБА. КОНЕЦ ОПИСАНИЯ ПРАВ И ОБЯЗАННОСТЕЙ Как применить эти условия договора к своим новым программам Если вы разрабатываете новую программу и хотите, чтобы она принесла максимально большую пользу широкому кругу людей, лучшим способом достижения этого является включение ее в состав свободного программного обеспечения, которое каждый может распространять и изменять в соответствии с данными условиями. Чтобы это сделать, добавьте к программе следующие уведомления. Надежнее всего добавить их в начало каждого исходного файла, чтобы наиболее эффективно сообщить об отказе от гарантий; и в каждом файле должна быть по крайней мере строка с «авторскими правами» и указанием на то, где можно найти полное уведомление. одна строка с названием программы и описанием того, что она делает Copyright (С) год имя автора Эта программа является свободным программным продуктом; вы можете распространять и/или изменять ее на условиях Общедоступной лицензии GNU в том виде, как это опубликовано Фондом свободного программного обеспечения; либо версии 2 Лицензии, либо (по вашему выбору) любой последующей версии. Данная программа распространяется в надежде, что она будет полезной, но БЕЗ ВСЯКИХ ГАРАНТИЙ; в том числе без подразумеваемых гарантий ПОЛЕЗНОСТИ или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ. Дополнительные подробности см. в Общедоступной лицензии GNU.
Пример использования 619 Вместе с данной программой вы должны были получить копию Общедоступной лицензии GNU; если вы ее не получили, напишите в Фонд свободного программного обеспечения: Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 20-111, USA. Добавьте также сведения о том, как связаться с вами по электронной или обычной почте. Если программа интерактивная, вставьте в ее вывод при запуске краткое уведомление, подобное данному: Gnomovision version 69, Copyright (С) год имя_автора Gnomovision поставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ; чтобы получить подробности, наберите vshow w'. Это свободный программный продукт, и вы можете распространять его при определенных условиях; наберите Nshow с', чтобы получить дополнительные сведения. Гипотетические команды 'show w' и 'show с' должны отображать соответствующие части Общедоступной лицензии. Конечно, используемые вами команды могут называться как-нибудь иначе, а не 'show w' и 'show с'; они могут быть даже щелчками мышью или пунктами меню - всем, что лучше подходит вашей программе. Вам следует также получить от вашего нанимателя (если вы работаете программистом) или учебного заведения, если оно имеется, заявление об «отказе от авторских прав» для программы, если это необходимо. Вот образец; измените имена: Yoyodyne, Inc., настоящим отказывается от всех авторских прав на программу "Gnomovision1 (которая работает;с компиляторами), написанной James Hacker. подпись Ту Coon, 1 апреля 1989 Ту Coon, вице-президент Данная Общедоступная лицензия не разрешает включать вашу программу в частные программы. Если ваша программа представляет собой библиотеку процедур, вы можете счесть более полезным разрешить компоновку частных приложений с данной библиотекой. Если это то, что вам нужно, используйте вместо данной Лицензии Малую общедоступную библиотечную лицензию GNU. Пример использования Данный раздел не является частью GPL. Здесь мы показываем комментарий с заявлением об авторских правах из программы GNU env: /* env - run a program in a modified environment Copyright (C) 1986, 1991-2002 Free Software Foundation, Inc.
620 Приложение С. Общедоступная лицензия GNU 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, 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 02111-1307, USA. */ Это обычное использование. Оно содержит следующие, в сущности, стереотипные элементы: Одну строку комментария с названием и описанием программы. В более крупных программах здесь может быть название и описание файла внутри программы. Заявление об авторских правах. • Два параграфа с объяснением и отказом от гарантий. Где получить копию GPL.
Предметный указатель ' (одинарная кавычка), в спецификаторах формата, 473-475, 488 - (тире) - (два тире) в nManginfoO, 479 в длинных опциях, 40 в качестве имени файла, 93,104,452,454 в качестве специального аргумента, 39 в опциях, 38-39, 40, 46 в правах доступа, 21, 144 в регулярных выражениях, 467 # (решетка) в качестве обозначения комментария, 234 в качестве приглашения, 232 в спецификаторах формата, 473 #!, в сценариях, 24,286 #ifdef, 35, 246, 290,337,462, 543, 550 $ (знак доллара) в качестве приглашения, 13 в качестве символа валюты, 462,470,472 в регулярных выражениях, 449-450 в спецификаторах формата, 486-487 % (знак процента), в спецификаторах формата, 472 0 (скобки), в регулярных выражениях, 450-451 , (запятая) в аргументах опций, 39 , в качестве десятичной точки, 470 . (точка) в nManginfoO, 479 в именах файлов, 440-441 в качестве десятичной точки, 463,470 в качестве текущего рабочего каталога, 26,130,135,137,140-141,164,205, 440-441,488 в регулярных выражениях, 449-450 в спецификаторах формата, 473 .. (две точки), родительский каталог, 130, 135,137,140-141,164, 205, 273, 440-441 Л (крышечка), в регулярных выражениях, 449-450 в корне смонтированной файловой системы, 226 / (прямой слеш), в качестве корневого каталога, 26,164, 225-227, 272 /dev/fd/XX, файлы, 315-317,331 /dev/random, файл, 437-438 /dev/urandom, файл, 437-438 /etc/fstab, файл, 234-239, 273 /etc/mtab, файл, 234-237, 237-239, 245-246, 273 /etc/vfstab, файл, 234 /proc/mounts, файл, 234-235, 237, 239, 273 /proc/self/cwd, файл, 256 /tmp, каталог, 23,396,422,457,488 : (двоеточие) в getopt(), 44, 46 в nlJanginfo(), 477 в optstring, 43 в переменной окружения PATH, 287 в регулярных выражениях, 467,496 ; (точка с запятой) в getoptJong(), 49 в nlJanginfo(), 477 ? (вопросительный знак), в getopt(), 44,46 (I (квадратные скобки), в регулярных выражениях, 451,467 \ (обратный слеш), для переноса строки, 77,81 (), макрос, 485-486, 488, 490,498 exit(), 293-295,314,330,349 J?xlt(), 293-295,330,349 GNU SOURCE, константа, 83 JOjvfprintfJnternal(), 590 {} (фигурные скобки), в регулярных выражениях, 451 | (вертикальная черта) в качестве конструктора конвейера, 27 в качетсве разделителя флагов, 550-553 |&, в gawk, 325
622 Предметный указатель "никаких произвольных ограничений ", принцип, 32-33, 36, 63, 77-78, 549, 553, 603 + (знак плюса) в nl_langinfo(), 479 в спецификаторах формата, 472-473 = (знак равенства), в аргументах опций, 40 > (знак больше) в качестве оператора, 116 в качестве приглашения, 13 » оператор, 116, 393 флаги для, 320-321 А a.out, формат Assembler OUTput, 24 abortO, 347, 357, 408, 423-424, 457, 537 acceptQ, 348 access(), 348, 391-393, 405 Acorn Advanced Disc Filing System, 229 actionJiandler(), 355-356 adb, отладчик, 535 addmntentQ, 237 Agans, David J., 18, 594, 597 aio_error(), 348 aio_return(), 348 aio__suspend(), 348 alarm(), 348, 364-365, 380-381,382, 514-515 alloca(), 86-87, 90 справочная страница, 86 alphasort(), 188, 203 Amiga Fast File System, 229 Andrew File System, 229 ANSI (Национальный институт стандартизации США), 9, 607 arg, библиотека, 62 argc, параметр, 41-46, 61 Argp, библиотека, 62 Argv, библиотека, 62 argv, параметр, 41-46, 61, 286-289,330 ASCII, кодировка, 493-494, 497 asctime(), 171-172,203,477 Assembler OUTput, см. a.out assertO, 407-411, 424, 517, 544, 595 AT&T, 14,43,387 atexitQ, 58, 293-295, 330, 431 atoi(), 434 Autoconf, 31, 34-35,475, 493 autofs, файловая система, 229 Automake, 475, 493 Autoopts, библиотека, 62 awk, программа, 319-320,325,448, 457, 568 версия GNU, см. gawk В b, в правах доступа, 25,144 Bash, оболочка, 13, 315,331, 539 be, команда, 112 Beebe, Nelson H.F., 18,456 Bell Labs, 36 BeOS, файловая система, 229, 231 Berry, Karl, 18 bg, команда, 334,366 bindO, 348 bindtextdomamO, 58, 487-488, 492,498 binfmtjnisc, файловая система, 229 Bloom, Benjamin, 606, 608 Bostic, Keith, 329 Bourne, оболочка (Борна), 13,336 break, оператор, 426-427 Brennan, Michael, 18 brk(), 85, 87, 90 Brooks, Fred, 607-608 BSD Fast Filesystem, 228-232, 241 BSD Unix, 30,120,131,133,145 dirfd(), 253 fchown() и fchmodO, 164 fts(), 264 getpgrpQ, 303-304 setreuid() и setregid(), 399 signal(), 341 stblocks поле, 215 timezoneO, 179 wait3() и wait4(), 300, 330 блокировка файлов, 500 владение файлами, 158 дампы ядра, 298 каталоги, 394 наборы групп, 387
Предметный указатель 623 отладчики, 535-536 сетевые базы данных, 195 сигналы, 343, 349, 351, 367, 381 файловые системы, 137, 228 функции сортировки, 188 bsd_signal(), 341, 346, 349, 381 bsearch(), 190-195, 198, 203 BSS (Block Started by Symbol), 64 BSS, области, 64-65 BSS, секции, 64-65 С с, в правах доступа, 25, 144 С, язык, 5 константа NULL, 68-69 перенос строк, 77 препроцессор, 544-545 см. также Оригинальный С, Стандартный С стандарты 1990, 1999 гг., 8, 34 тип символьных констант, 216 C++, язык main(), 292 имена, 536 препроцессор, 544-545 присвоение значения указателю, 69 программы GNU, 30 прототипы функций, 28 сортировка массивов объектов, 187 Стандарты 1998, 2003 гг., 8-9, 34 ' тип символьных констант, 216 элементы const, 65 calloc(), 67-68, 75-76, 89, 269 Сареу, Т., 609 cat, программа, 92-93, 103-106 версия GNU, 108 версия V7, 106-108, 153 catdir, программа, 138-140 catgets(), 461,477, 497 ccmalloc, библиотека, 591-592 cd, команда, 26, 253, 317 CD-ROM, 25, 228, 230, 232, 235-236 cfgetispeed(), 348 cfgetospeedQ, 348 cfsetispeed(), 348 cfsetospeed(), 348 char, тип, 69 chdirO, 253-254, 272, 273, 349 check_salary(), 567 chgrp, программа, 22 child(), 378 chmodO, 159,163,165,349 chmod, программа, 22,113, 387 chown(), 158-159,163,165,198, 349,395 chown, программа, 22, 256 chroot(), 272-273 справочная страница, 272 cleanup(), 342 clearenv(), 54 справочная страница, 54 cIoc,k_gettime(), 349 dose(), 91,101, 116, 280, 306, 308, 331,349, 507 closedir(), 138,164 close-on-exec, флаг, 318-320,324, 329, 331 Cocker, Gail, 18 coda, файловая система, 229 COFF (Общий формат объектных файлов), 24 Coherent, файловая система, 230-231 Collyer, Geoff, 18, 70,100,361 compar(), 221 compiIe_pattern()> 453-454 connect(), 349 const, ключевое слово, 29, 65 cont, команда (GDB), 541, 548, 598 continue, оператор, 426 copyright(), 485 Cox, Russ, 18 cp, программа, 289 cpio, программа, 153,159,161 cramfs, файловая система, 229, 232 creatO, ИЗ, 116-117,121,127, 279,310, 349 csh, справочная страница, 13 ctime(), 171-172,179, 203, 213, 476 D d, в правах доступа, 21 dbug, библиотека, 568-574, 599 dbx, отладчик, 535 dcgettext(), 481
624 Предметный указатель ddd, отладчик, 535 debug_dummy(), 568 delete, оператор, 67, 69 DeMaille, Akim, 18 devfs, файловая система, 229 devpts, файловая система, 229 df, программа, 240, 246 dgettext(), 481 diff, программа, 33-34 difftimeO, 168,184,203 direct, структура, 137 dirent, структура, 137-143, 164, 188, 203, 44? dirfd(), 253 dmalloc, библиотека, 581-585, 591-592, 599 do_input(), 547 do_statfs(), 252 do_statvfs(), 244 Drepper, Ulrich, 18, 361 DST (daylight-saving time - летнее время), 171,178-181 du, программа, 256 dup(), 310-315, 330-331, 349, 400-401 dup2(), 310-315, 319-320, 330-331, 349 DVD, 230, 235 E EBCDIC, кодировка, 493 echo, программа, 37-38, 41-42, 45 ed, редактор, 397, 425, 448-449 ef, сценарий, 579-580 efence (Electric Fence), библиотека, 576-581, 591-592,599 справочная страница, 580-581 efs, файловая система, 229 egrep, программа, 448-449 ELF (Extensible Linking Format), 24, 286 Emacs, редактор, 31, 33, 448-449, 539, 603 emp_name_id_compare(), 183, 525-526 employee, структура, 183-187, 192-195, 504, 523-524, 525 ENABLE_NLS, константа, 484, 486, 488 endmntent(), 237 endp wen t(), 197-198 env, программа, 54, 55-61 environ, переменная, 54-55, 60, 286 Erickson, Cal, 590-591 errno, переменная, 69, 94,101,104,105, 120, 128,132,134-135,138,157,163,168, 202, 253- 254, 284, 286, 293, 295, 296, 322, 326, 342-343, 345, 349, 381, 393, 402, 442, 500, 506, 518 значения, 94-97 проверка, 97 справочная страница, 97 Ersoy, Alper, 18 euidaccess(), 393,405 —exclude, опция, 439 execO, 285-290,318, 330, 380-381,383,400-401, 403 execlO, 287-288, 327 execle(), 287-288,349 execlp(), 287-288, 298,403 execv(), 287-288, 442 execve(), 286, 349 справочная страница, 286 execvp(), 61, 287-288,314, 403, 442 exit(), 293-295, 297, 330, 423, 507 EXrT_FAILURE, константа, 291 EXIT_SUCCESS, константа, 291 ext, файловая система, 232 ext2, файловая система, 228-229, 231, 236,395 ext3, файловая система, 228-229, 231,236, 395 Extensible Linking Format, см. ELF Extensible Markup Language, см. XML F factorial(), 571 FAT, файловая система, 228, 230-231, 273 fchdir(), 253, 273 fchmod(), 159, 163-164, 349, 511 fchownO, 158, 163-164,198, 349 fclose(), 237 fcntlO, 317-324,331,349, 501,502-507, 508-509, 529-530 справочная страница, 317, 321, 502, 504 флаги для, 320-321 FD_CLOEXEC, флаг, 319 fdatasync(), 119,121,349 fdopenQ, 328 fflushO, 119,543 fg, команда, 334, 366 fgets(), 79
Предметный указатель 625 fgrep, программа, 38 FIFO (first-in first-out) файлы, 144, 308-310, 331,361-362 не блокирующий ввод/вывод, 322-324 пустые, 322-323 / создание, 309 удаление, 309 File Transfer Protocol, см. FTP file_interesting(), 440-441 filenoO, 102, 108,119, 507 find, программа, 153, 256, 439 справочная страница, 153 finish, команда (GDB), 540, 548 first-in first-out, см. FIFO файлы Fish, Fred, 18,569 flags2str(), 550-554 flock(), 501, 507-508, 529-530 справочная страница, 508 flock, структура, 318, 502-505, 511 fnmatch(), 439-441, 458 fopen(),218,420 fork(), 277-281, 285, 298-299, 318, 326, 329-330, 349,367,380-381,383,403 format_num(), 369, 376 fortune, программа, 431 fpathconf(), 348 fprintf(), 98, 340, 543 fpsync(), 119 Free Software Foundation, см. FSF free(), 67, 69,70-72,73, 76, 89,157,188, 255,415- 416, 431, 500, 529-530, 568, 574-575, 585, 588 FreeBSD, файловая система, 231 FSF (Free Software Foundation), 10,16, 536 стиль форматирования кода, 31 fstat(), 108, 145, 148, 153-154, 163, 254, 280, 348,392 fstatfs(), 248-252, 273 fstatvfs(),241,246, 273 fsync(), 119,121,348 FTP (File Transfer Protocol), 272 ftp, программа, 15 ftruncate(), 120,163,348 fts(), 264 ftw(), 256-257 FTW, структура, 259, 265 21 -159 FTW__CHDIR, флаг, 257-258, 262 full_write(), 163 futime(), 164 G gawk, программа, 10, 31-32, 77, 320, 325-329, 389, 426, 463, 539-558, 560-564, 568, 576 двусторонние каналы, 325 форматирование числовых величин в, 474-476 GCC (GNU Compiler Collection), 30, 49, 86, 534, 598 GDB (GNU Debugger), 34, 535-542, 544-548, 568, 575, 583, 586-587, 598 . дистрибутивы, 542 макросы в, 544-545, 557-558 макросы в, 545-546, 557-558 Gemmellaro, Anthony, 18 Gemmellaro, Faye, 18 genflags2str(), 551-553 getcwdO, 254-256, 273 справочная страница, 256 getdelim(), 83-84, 90 getdents(), 141 справочная страница, 141 getdtablesize(), 99,103,121, 262, 279 getegid(), 348, 389, 404 getenv(), 53, 61 geteuid(), 348, 388, 404 getgid(), 348, 388, 404 getgroups(), 348, 388-389, 404 getitimer(), 514-518, 529-530 справочная страница, 515 getline(), 83-84, 90, 455 getmntent(), 237-240, 273 getname(), 214 getopt(),39,43-46,61,571 версия GMJ, 39, 43, 45-46, 51, 61 справочная страница, 46 getoptJongO, 31, 39-40, 43, 46-52, 55, 58, 61, 488, 571 версия GNU, 51 getopt_long_only0, 46, 61 getpeernameO, 348 getpgidQ, 303-304, 330-331
626 Предметный указатель getpgrp(), 303-304, 330-331, 348 getpidO, 281-283, 330, 348 getppid(), 281-283, 330, 348 getpwent(), 197-198 getpwnam(), 197-198 getpwuid(), 197-198 getresgid(), 402 getresuid(), 402 getsid(), 305 getsockname(), 348 getsockoptQ, 349 gettext(), 481-482, 483-487, 498 gettext, программа, 461-462, 480-493, 498 gettext.h, файл, 484-486 gettext_noop(), 485, 490 gettimeofdayO, 512-513, 529-530 справочная страница, 512 getty, программа, 400-401 getuid(), 349, 388, 404 GID (ID группы), 21-22, 36, 115, 135, 146, 159, 196,199,203,385-386 маска для, см. setgid, бит реальный, 386-393, 396-405 сохраненный набор, 387,389,400-402, 404-405 эффективный, 386-405 gid^t, тип, 147 Glib, библиотека, 202 GLIBC (библиотека С GNU), 31, 35, 51, 82 euidaccess(), 393 libintl.h, 486 rand(), 434 TEMP_FAILURE_RETRY(), 345-346 значения errno, 94-97 значения fjlag, 243, 246-247 расширения glob(), 442-444 суперпользователь в, 386 флаг FTWCHDIR, 257-258, 262 glob(), 441-446, 458 g!oberr(), 446 globfree(),441,444,447 GMT (Greenwich Mean Time), 160 gmtime(), 169-171,203 GNOME, проект, 202 GNU С библиотека, см. GLIBC ^ GNU Compiler Collection, см. GCC GNU Coreutils, 15, 42, 57, 120, 128 du, 264-272, 274 fts(), 264 install, 298 pathchk, 392 . safe_read() и safe_write(), 344-345 sort, 342 utime(), 161 wc, 414-415 xreadlink(), 156 дистрибутив, 59 GNU Gengetopt, библиотека, 62 GNU GPL (Общедоступная лицензия GNU), 16,30,612-620 GNU Lesser General Public License, 51 GNU, отладчик, см. GDB GNU, программа, 10, 30-34, 51 длинные опции в, 40 обертывающие программы в, 82 GNU, проект, 10 GNU, стандарты кодирования, 30-36, 40, 71, 76, 99, 289 GNU/Linux,5,17 /dev/fd/XX, файлы в, 315-317 /proc/self/cwd, 256 chroot(), 272 clearenv(), 54 dirfd(), 253 ftw(), 256-257 remove(), 132, 164 rsync, 14 rusage, 301 signal(), 341 statfs() и fstatfs(), 248-252 versionsort(), 188 wait3() и wait4(), 300, 330 выделение времени в, 417-418 вытесняющая многозадачность в, 283 дампы ядра в, 298 дистрибутивы, 15, 229, 232, 334, 409, 461,497,578
Предметный указатель 627 каталоги в, 394 команда с99, 292 команда gcc, 292 локали в, 463 монтирование в, 141, 225 номера узлов в, 227 операция переименования в, 131 отладчики в, 536 размер блока в, 146 сведения о часовых поясах в, 180 сигналы в, 336-337, 349, 382 стандартные функции в, 84 суперпользователь в, 386 тип time_t, 167 типы файлов в, 149 файловые системы в, 124, 137, 228-233, 273 форматирование числовых значений в, 474 форматы файлов в, 24 Эпоха в, 160 GoId,Yosef, 18 goto, оператор, 424 Greenwich Mean Time, см. GMT grep, программа, 448-456 groff, программа, 30 group, структура, 200 gstat(), 211, 218-219 GTK+, проект, 202 gzip, программа, 16 H handler(), 340, 343 hasmntopt(), 237 Hayes, John R., 606, 608 Heisenberg, Werner, 411 —help, опция, 31, 40, 59 Hesiod, 195 Hierarchical File System, 229 High Performance File System, 229 Hoare, C.A.R., 7, 18, 181, 407, 410 HOME, переменная окружения, 27 HIRD, ядро, 31 21* I il8n, см. интернационализация IBM, 230, 446, 493 IEEE Standard 1003.1-2001, 9 ifindO, 191 IFS, переменная окружения, 403 inetd, программа, справочная страница, 362 init, процесс, 26, 280, 360, 367, 400-401 init_groupset(), 389-390 initstateO, 435-436 Insight, отладчик, 536 install, программа, 298 InterMezzo, файловая система, 229 International Organization for Standardization, см. ISO intro, справочная страница, 43, 97,142 IPC (межпроцессное взаимодействие), 145, 305, 361 использование сигналов для, 361, 382 Irix, система, 229 isatty(), 202-203 справочная страница, 202 ISO (International Organization for Standardization)^ ISO 9660, файловая система CD-ROM, 228, 230-231,273 ISO С, см. Стандарт С ISO/IEC 14882, Международный стандарт, 8-9 ISO/IEC 9899, Международный стандарт, 8 iswalnum(), 494 isw!ower(), 495 itimerval, структура, 515-517, 529-530 J Java, язык, 28 Jedi Knight, 18 Journaled File System, 230 Journaled Flash Filesystem, 230 К K&R С, см. Оригинальный С Karels, Michael J., 329 KDE 590-591 Kenobi, Obi-Wan, 17 Kerberos, сетевая база данных, 195
628 Предметный указатель Kernigan, Brian W., 598, 603 Kernighan, Brian W., 5, 18, 28, 32, 75, 565-566 kill(), 328, 347-348, 356-357, 359-361, 382 kill, программа, 338, 366 killpg(), 359-360,362, 382 Kirsanov, Dmitry, 18 Kirsanova, AHna, 18 Knuth, Donald E., 202, 456, 598 Konquerer, веб-браузер, 590-591 ksh, оболочка Корна, 325,330-331 ksh88, оболочка, 315 ksh93, оболочка, 13, 315 L 11 On, см. локализация LANG, переменная окружения, 463 Lave, Jean, 608 lbuf, структура, 207, 211, 218-222 недействительная, 212 LC_ALL, переменная окружения, 463 lchmod(), 159 lchown(), 158, 164 Iconv, структура, 468-471, 475-477 Id, программа, 23 LD_PRELOAD, переменная окружения, 589-590, 582 LDAP (Lightweight Directory Access Protocol), 195 Lechlitner, Randy, 18 LED, 595 Lehman, Manny, 18 libintl.h, файл, 486 Lightweight Directory Access Protocol, см. LDAP limits.h, файл, 138 line, программа, 280 link(), 128-130, 164,349 link, программа, 128-130 lint, программа, 592-593, 599 Linux Journal, 30, 590-591 Linux, см. GNU/Linux list, команда (GDB), 539 Iisten(), 349 In, программа, 127-128, 134, 289 locale, программа, 461 справочная страница, 496 localeconv(j, 468-471,497 localtimeO, 169-171,179, 203 lockf(), 501, 502-507, 529-530 login, программа, 399 Iongjmp(), 259, 425-427, 427-431, 457, 570, 674 Is, команда, 144-145, 151-152, 202, 440-441 версия V7, 205-222 современные версии, 205-206, 222 lsearch(), 407-411 IseekO, 109-110,117,120,163, 215, 279,305,349 значения для, 109 IstatO, 145,148,154-155,157,165, 256, 349 lutimeO, 164 M MacOS X Netlnfo, сетевая база данных, 195 mainO, 26,43, 58,61,295-296,297,330-331, 507 объявление, 41 статус завершения процесса в, 93 main, программа, 292-293 majorO, 151,165,213 make, программа, 493 версия GNU, 77-82 makedevO, 152 Makefile, 77 nrakename(), 217, 222 mallocO, 64, 67-70,71-77, 81, 85, 89,155-157, 188, 219, 254-255, 390, 429, 467, 500, 520, 522-523, 568, 574-575, 580, 585, 590-591 приведение типа возвращаемого значения, 69 справочная страница, 68 malloc, замещающие библиотеки, 590-591 MALLOCJTRACE, переменная окружения, 575 man, команда, 6,13 manage(), 376-380 Marti, Don, 18 mblen(), 495 mbrlenO, 495 mbrtowcO, 495 mbsrtowcsO, 495 mbstowcs(), 495 mbtowcO, 495 McGary, Greg, 18 Mcllroy, Doug, 18
Предметный указатель 629 McKusick, Marshall Kirk, 329 memalignQ, 499-500, 529-530 memccpyO, 412, 457 memchr(), 414-415, 457 memcmp(), 413-414, 457 memcpyO, 412-413, 457 Memishian, Peter, 18 memmove(), 35, 412-413, 457 memset(), 70, 75, 411, 457 memwatch, библиотека, 591-592 Meyering, Jim, 18, 42, 52, 157, 264 Microsoft Windows соглашение по завершению строки, 79 " файловые системы в, 124, 228, 230-231 Эпоха в, 160 mingetty, программа, 400-401 Minix, файловая система, 230-231 minor(), 151,165,213 mkdir(), 134-135, 164,349 справочная страница, 135 mke2fs, программа, 124 mkfifoO, 309, 330-331, 349 справочная страница, 309 mkfifo, программа, 144 mkfs, программа, 124 mknod, программа, справочная страница, 152 mkstemp(), 419-421, 457 mktemp(), 415-419, 420 mktime(), 177-179, 179,203 mntent, структура, 237-240 Moraes, Mark, 590-591 mount(), 225 mount, программа, 225, 230, 232-237, 273,387, 509 справочная страница, 231, 236 mpatrol, библиотека, 590-591 MS-DOS, 228 msdos, файловая система, 230-231 mtrace(), 575-576 mtrace, программа, 575-576, 590-592, 599 muntrace(), 575-576 Murakawa, Yasushi, 609 mv, программа, 130, 289 N N_(), макрос, 485-486, 488, 490, 498 nanosleep(), 518-519, 529-530 nbIock(), 214-215 NCP, протокол для NetWare, 230 NetBSD, файловая система, 231 Network File System, см. NFS Network Information Service, см. NIS new, оператор, 67, 69 newfs, программа, 124 next, команда command (GDB), 540, 598 NeXTStep, система, 231 NFS (Network File System), 228, 273 nftwO, 246-247, 256-264, 273-274 справочная страница, 258 флаги для, 258-262 функция обратного вызова для, 258, 262-263 частная версия, 265 ngettext(), 482-483, 488, 498 nice(), 283-285, 330 NIS (Network Information Service), 195 njamd (Not Just Another Malloc Debugger), библиотека, 591-592 nIJanginfo(), 477-479, 488, 497 NLS (native language support - поддержка родного языка), 461, 497 , nodots(), 190 Norvig, Peter, 18, 605 Novell, 230 NTFS, файловая система, 230-231 NUL, символ, 23, 32-33, 79, 81 NULL, константа, 68-69, 89, 409 О OJVONBLOCK, флаг, 321-324, 331 obstacks, 33 off_t, тип, 109-110 Open Group, The, 9 open(), 91,101-103,113,116-119,121,127,141, 154,253,279,310,315,349,392-393 справочная страница, 119 флаги для, 101, 116-119, 121,321-323 OpenBSD, файловая система, 231 opendirQ, 138,164, 262, 441-442
630 Предметный указатель OpenOffice, 590-591 openpromfs, файловая система, 230 Open VMS, файловая система, 125 Opt, библиотека, 62 optimalJbufsize(), 560-564 option, структура, 46 os_close_on_exec(), 329 other, категория пользователей, 21, 113-116, 386 маска для, 149-150 • Р р, в правах доступа, 144 parse_debug(), 560, Pascal, язык, 424 passwd, структура, 197-198 PATH, переменная окружения, 27, 287, 330, 403 pathchk, программа, 392 pathconf(), 349 pause(), 349, 362-365, 382, 511 PDP-11, 20, 126, 222, 395, 434 pentry(), 212 Perl, язык, 448-449 perror(), 33, 97, 120 pfatal_with__name(), 83 PGID (групповой ID процесса), 278, 296-305, 330-331 P1D (ID процесса), 25-27, 278, 281-283, 286, 305,330-331 завершившегося процесса, 296, 298 использование в случайных числах, 436-437 обертывание, 280 родителя, см. PPID pid_t, тип, 277 Pike, Rob, 35, 598, 603 Pinard, Fran3ois, 18 pipe(), 305-307, 326, 330-331, 349 PIPEJ3UF, константа, 322-323 Plan 9 From Bell Labs, 62,116 pmode(), 216 po!I(), 349 Popt, библиотека, 62 POSIX, стандарт, 9 _exit(), 293-295 bsd_signal(), 341 environ, переменная, 54-55 ftw(), 257 isattyO, 202 printfO, 486-487 rusage, 301 waitpid(), 296 блокировка файлов в, 500 владение файлами в, 22 значения errno, 94-97 именованные константы для прав доступа в, 114-115 интерфейсы библиотечных и системных вызовов в, 35 каталоги в, 395 классы символов в, 467, 496 константа PIPE_BUF, 322-323 маски типов файлов и прав доступа в, 149-150 относительные приоритеты в, 285 поле st_blocks, 215 расширения, 9 ADV, 500 FSC, 120 SIO, 120 TMR, 518 XSI, 9, 84, 120, 137, 240, 297-298, 303 сведения о группе процесса в, 303-304 ' сигналы в, 343,349,351,354,359-361, 367,381 совместимость с, 31, 52 соглашения по опциям в, 38-40, 49 суперпользоатель в, 386 THntimet, 168 файловые системы в, 137 флаг FD_CLOEXEC, 319 флаги для ореп(), 116-119, 321-323 часовые пояса в, 178-179 posix_memalign(), 500, 529-530 posix_trace_event(), 349
Предметный указатель 631 POSIXLY_CORRECT, переменная окружения, 46, 52 PPID (ID родительского процесса), 25, 278, 281-283,330 print_emp(), 527 print_employee(), 193 print_group(), 200-201 print_mount(), 240 printfO, 102,264,369,429,472-476,488,497-498, 595 версия POSIX, 486-487 справочная страница, 474 proc, файловая система, 230 process_file(), 269, 270 PROM, файловая система, 230 ps, программа, 64 pselectQ, 349 ptrdiff_t, тип, 68 putenv(), 54, 61 версия GNU, 54, 60 pwd, программа, 317 Q QNX4, файловая система, 230 qsort(), 181-190, 203,211 Quarterman, John S., 329 Quicksort algorithm, 181 quote_n(), 130 R г, в правах доступа, 21 Rago, Stephen, 329 raise(), 338,342,347-348,356-357,360,366,381 Ramey, Chet, 18 ramfs, файловая система, 230 rand(), 432-434, 458 версия GLIBC, 434 справочная страница, 434 сравнение с random(), 436-437 random(), 435-437, 458 справочная страница, 435, 437-438 сравнение в rand(), 436-437 Raymond, Eric S., 603, 607 read(),91,103-106,117-119, 121, 141,279,308, 322-324, 342-343, 348 read, встроенная команда оболочки, 280 readdir(), 138-141,164,211,217-218,439-440, 441-442 версия GNU/Linux, 141 readline(), 77, 83 readline, библиотека, 539, 603 readlink(), 155-157,165, 256,348 readstring(), 78 reallocO, 68,72-76, 81,89, 254, 269,520,522-523, 574-575 версия GNU, 76 версия стандартного С, 73 recurse(), 538 recvO, 348 recvfrom(), 348 recvmsg(), 348 regcompO, 448-451, 458 regerror(), 448-449 regexecQ, 448-451, 458 regfreeO, 448-449 register, ключевое слово, 42, 62 reiserfs, файловая система, 230 Remote File System, см. RFS remove(), 132,309 версия GNU/Linux, 132, 164 rename(), 131,164,348 rewinddirO, 138,164 RFS (Remote File System), 228 Ritchie, Dennis M., 5,18, 28, 32,387 rm, программа, 23 rmdir(), 132,134-135,164, 348 rmdir, программа, 136 Robbins, Miriam, 18 Rock Ridge, расширения, 230 romfs, файловая система, 230-231 root (суперпользователь), 22-23,158-159, 232, 236, 272, 360, 386-389, 391, 396-405, 512 rpl_utime()> 163 rsync, программа, 14 Rueda, Carlos, 609 run, команда (Gt)B), 539 rusage, структура, 300-301,330-331 S S_IS xxx 0, макрос,. 148, 150,165 sa__flags, поле, 354-355,361, 367
632 Предметный указатель SA_NODEFER, флаг, 354-355 SAJMOMASK, флаг, 355 SA_ONESHOT, флаг, 355 SA_RESETHAND, флаг, 355 safe_read(), 163, 344-345, 346 safe_write(), 344-345 sbrk(), 85, 87-88, 90, 430 scandir(), 188-191,203 scanf(), 186 SCO UnixWare Boot Filesystem, 229 sdb, отладчик, 535 sed, программа, 448-449, 457 seekdir(), 143, 165 select(), 216,348 sem_post(), 348 sendQ, 348 sendmsg(), 348 sendto(), 348 setegid(), 398, 401, 404-405 setenv(), 53, 61 seteuid(), 398, 401, 404-405 setgid(), 349, 398-399, 400-402, 404-405 setgid, бит, 149, 242-243, 254, 387-388, 399-401, 404-405,509-512,529-530 для каталогов, 393-395, 404-405 setgroups(), 396-397, 400-401, 404-405 setitimer(), 365, 514-518, 529-530 setjmp(), 425-427, 427-431, 457, 570, 573-674 setlocale(), 58, 462, 463-464, 488, 496, 497 справочная страница, 462 setmntent(), 237 setpgid(), 304-305, 330-331, 349 setpgrp(), 304-305, 330-331 setpwent(), 197-198 setregid(), 398-399, 401, 404-405 setresgid(), 402, 405 setresuid(), 402, 403, 405 setreuid(), 398-399, 402, 403, 404-405 setsid(), 305, 349 setsockopt(), 349 setstate(), 435-436 settimeofdayQ, 512 setuid(), 349, 398, 400-402, 403, 404-405 setuid, бит, 149, 242-243, 254, 386-388, 399-401, 404-405 запуск в качестве root, 403, 405 Seventh Edition Research UNIX System, см. V7 Unix Seward, Julian, 18 shutdown(), 349 si_code, поле, 356 sig_atomic_t, тип, 347, 349, 382 sigaction(), 343, 347-348, 351, 354-359, 361-362, 364-365, 382-383 справочная страница, 354, 356 sigaction, структура, 354-355, 358, 367, 382 sigaddset(), 349, 352, 361, 382 sigaltstack(), 355 sigdelset(), 349, 352-353, 361, 382 sigemptysetO, 349, 352-353, 361, 382 sigfillset(), 349, 352-353, 358, 361, 382 sighold(), 350-351, 352 sigignoreQ, 350 siginterrupt(), 359, 361 sigismember(), 349, 352-353, 361, 382 siglongjmpO, 427-428, 429, 457 signal(), 335-338, 348-349, 358, 365, 381-382 версия BSD, 341 версия GNU/Linux, 341 справочная страница, 335, 337 sigpauseQ, 349,350, 364-365 sigpendingO, 349, 353-354, 358,361,382 sigprocmask(), 349,353, 361, 382 sigqueue(), 348, 356 sigrelse(), 350-351 sigset(), 348,350-351,351 sigset_t, тип, 352-353, 361, 382 sigsetjmp(), 427-428, 429, 457 sigsuspendO, 348, 353-354, 361,364,379, 382 sigvec(), 351 size, программа, 65, 89 sizet, тип, 68 sizeof, оператор, 69,112, 553 sleep(), 282-283, 348, 365, 382, 518 SMB, файловая система, 230 socket(), 348 socketpair(), 348 Solaris, 273 gettext, 461 дампы ядра в, 298 каталоги в, 394
Предметный указатель 633 монтирование в, 225 сигналы в, 339-340, 349, 375, 381 файловая система в, 231, 234 форматирование числовых величин в, 474 функции сортировки, 188 sort, программа, 342 справочная страница, 181 Southern Storm Software, 14 SPARC, система, 230-231 Spencer, Henry, 18, 36, 137 splint (Secure Programming Lint), программа, 592-593, 599 sprintf(), 474 srand(), 432, 458 srandom(), 435-436, 458 ssizet, тип, 83, 104 stctime, поле, 147 stmode, поле, 146-149, 153 st_size, поле, 146, 155,157,165 Stallman, Richard M., 36, 76 stat(), 145, 148, 152,153-154, 163, 165,177, 201, 214, 220, 246, 348, 392-393, 404-405, 441-442, 514-515,566-567 справочная страница, 147 стоимость, 209 stat, структура, 108, 145-148, 159-160, 165, 167, 207, 215, 220-221, 222, 225, 256, 274, 280, 513-514,560-561 statfs(), 248-252, 273 statfs, структура, 248-252 statvfs(), 240-248, 273 statvfs, структура, 241-243, 244, 246-248 stderr, переменная, 102 stdin, переменная, 102 stdio.h, файл, 79,104,106,119,138,347-348,419 stdlib.h, файл, 68 stdout, переменная, 102 step, команда (GDB), 540, 548, 598 Stevens, W. Richard, 329 stopme(), 568 strcasecmp(), 468 strcmp(), 137,182-183, 222, 413, 464-468, 497 strcoll(), 188,464-468,497 strcpy(), 413 strdupQ, 84, 90, 464 strerror(), 98, 105,120 strfmon(), 471 -474, 488, 497 strftime(), 172-177,179, 181, 203, 471, 476-477, 488,497 strip(), 298 strip, программа, 67 strncmpO, 468 Stroustrup, Bjarne, 544-545 strtoul(), 565 strverscmpO, 188 strxfrm(), 467-468, 497 Sun Microsystems, 195, 228, 394 symlink(), 134,154,164,348 справочная страница, 134 sysconfQ, 348 sysctl, программа, справочная страница, 334, 537 sysIog(), 565-566 System HI, 43 FIFO в, 309 исполняемые файлы в, 289 отладчики в, 535 System V, 145 fchown() и fchmod(), 164 ftw(), 256-257 UID в, 386 блокировка файлов в, 500 владение файлами в, 159 каталоги в, 393-394 поле st_blocks, 215 сигналы в, 339-340,343,350,351,361, 367,382 файловые системы в, 228, 234 файловые системы, 230-231 sysv, файловая система, 230-231 Т t, в правах доступа, 23 Taber, Louis, 18 tar, программа, 16, 153,159,161 Taub, Mark, 18 tcdrain(), 348 tcflow(), 348 tcflush(), 348
634 Предметный указатель tcgetattr(), 348 tcgetpgrp(), 349 tcsendbreak(), 349 tcsetattr(), 349 tcsetpgrp(), 349 tcsh, справочная страница, 13 tdelete(), 521-522, 529 tdestroy(), 521-522, 528-529 telldir(), 143,165 TEMP_FAILURE_RETRY(), макрос, 345-346, 349 tempnam(), 415-416 textdomainO, 58, 480-481, 488, 498 tfindO, 521-524 Thompson, Ken, 116 Time Sharing Option, см. TSO time(), 168, 177, 203, 349, 512 time J, тип, 147, 159-160,167-169, 177-179, 203,513-514 timer_getoverrun(), 349 timer_gettime(), 349 timer__settime(), 349 timerclear(), макрос, 512 timercmpO, макрос, 512 timerisset(), макрос, 512 timesO, 349 timespec, структура, 518, 519, 529-530 timeval, структура, 301, 512-513, 513-518, 519, 529-530 timezone(), 179 tm struct, 169-171, 177,203 tm_isdst, поле, 171 TMPDIR, переменная окружения, 421-423,457 tmpfileO, 293,419,457 tmpfs, файловая система, 230 tmpnam(), 415-416 touch, программа, 177 версия GNU, 161 версия Unix, 163 trap, встроенная команда оболочки, 335-336 troff, программа, 31 Tromey, Tom, 18 truncateQ, 120 tsearch(), 521-524 TSO (Time Sharing Option), 21 tty, см. терминалы TUHS (The UNIX Heritage Society), 14 tune2fs, программа, справочная страница, 231 Turing, Alan, 407 Turski, Wladyslaw M., 18 twalkO, 521-522, 525-529 two_way_open(), 325 type2str(), 251-252 TZ, переменная окружения, 178 tzset(), 178-180, 203 и UDF, файловая система, 230-231 UID (ID пользователя), 21-22, 36,115, 146, 196,198-199,203,385-386 маска для, см. бит setuid реальный, 360, 386-393, 396-405 сохраненный набор, 387,389,397-402, 404-405 эффективный, 360, 386-393, 396-405 uid_t, тип, 147 ulimit, встроенная команда оболочки, 99-100 umask(), 115, 121,349 u mask, встроенная команда оболочки, 114-115 umasks, 115,117,278 umount, программа, 225, 233, 234, 237, 273 umsdos, файловая система, 230-231 uname(), 349 Unicode, набор символов, 461, 494 unistd.h, файл, 100 Unix chroot(), 272 date, 175 ftw(), 256-257 архивы старых версий, 14 вытесняющая многозадачность в, 283 квантование времени Btime slicing in, 417-418 монтирование в, 141 номера индексов в, 227 программы в, 33 размер блока в, 146 соглашение по завершению строки в, 79 стандартные функции в, 85 тип timet, 167
Предметный указатель 635 файловые системы в, 124 форматы файлов в, 24 чтение каталогов в, 137 Эпоха в, 160 UNIX Heritage Society, см. TUHS unlink(), 131-132, 164, 309, 349 unref(), 558-559 unsetenv(), 54, 61 UPS (бесперебойный источник питания), 118 URL «Проект GNU», статья, 36 ANSI, 9 Argp, 62 Argv, 62 Autoopts, 62 ccmalloc, 590-591 dbug, 569 ddd, отладчик, 535 dmalloc, 581 Electric Fence, 578 Free Software Foundation, 536 GNOME, проект, 202 GNU Gengetopt, 62 GNU gettext, 493 GNU grep, 457 GNU Make, 77 Hints On Programming Language Design, 410-411 Insight, 536 InterMezzo, 229 ISO, 9 Linux Journal, 30, 590-591 malloc, 590-591 memwatch, 591-592 mpatrol, 590-591 Notes on Programming in C, 35 Open Group, 9 . Opt, 62 Plan 9 From Bell Labs, 62 Popt, 62 Recommended С Style and Coding Standards, 36 rsync, веб-страницы, 14 Southern Storm Software, 14 splint, программа, 592-593 Teach Yourself Programming in Ten Years, 605, 609 The Art of Computer Programming, 456 TUHS, 14 Unicode, 494 United States Patent and Trademark Office, 387 Valgrind, 590-591 WINE, эмулятор, 590-591 XFS,231 yamd, 591-592 архив библиотеки CVS GNU C, 51 Защита содержимого файла данных, 387 правила отладки, 594, 597-598 . проект GNU, 10 проект GTK+, 202 сравнение Ext2fs и Xiafs, 232 стандарты программирования GNU, 31 usage(), 57, 456 usbfs, файловая система, 230 UTC (Coordinated Universal Time - всеобщее скоординированное время), 160,179 UTF-8, кодировка, 33, 461 utimbuf, структура, 160,163 utime(), 159-163,165,177,349, 513-514 utime_null(), 163 utimes(), 163, 513-514, 529-530 V V6 Unix, 446 V7 Unix, 8,14, 29 cat, 153, 106 rand(), 434 rmdir, 136 wait(), 296 дистрибутив, 41 индексы в, 126 каталоги в, 394 команда Is, 205-222 отладчики в, 535
636 Предметный указатель сигналы в, 339-340, 343, 349, 351, 362 сопоставление имен с ID в, 195 файловая система в, 228 Valgrind, отладчик, 585-592, 599 van der Linden, Peter, 18 VAX, система, 228 —verbose, опция, 31, 40 Veritas VxFS journaling filesystem, 231 —version, опция, 31, 59 versionsort(), 188 vfat, файловая система, 231, 273 vi, редактор, 448-449, 539, 603 vim, редактор, 448-449 void *, тип, 68, 182,521-522 volatile, ключевое слово, 29, 347, 428 W xreadlink(), 156,157,256 xrealloc(), 82 Y yamd, библиотека, 591-592 A адресное пространство, см. память анализаторы логики, 595 аргументы, 37-40 недействительные, 96 необязательные, 39-40, 46 отсутствующие, 44-46 содержащие разделители, 37, 39-40 списки, 94 архивы, 161 атака отказа в обслуживании, 560-561 атомарная запись, 323 Б безопасность, 163,196, 387-388, 402-405, 410- 411,420,457 бесконечные циклы, 430-431 бесперебойный источник питания, см. UPS библиотеки, 34 общего назначения, 341 разделяемые, 67, 320, 579 стандарт POSIX для, 34 биты прав доступа, 21, 216, 222, 254 константы для, 114-115 маски для, 149-150 по умолчанию, 115 блоки, 125-126,225 вспомогательные, 206 загрузочные, 240 испорченные, 227 копирование, 412-413 размер, 146, 241 сравнение, 413 суперблок, 240 фрагменты, 241 функции для, 411 число, 147,206,215,241,273 блокировки, 500-502, 529-530 w, в правах доступа, 21 wait(), 283, 296-300, 330-331, 343, 349, 367-368 wait3(), 300-301, 330-331 wait4(), 300-301, 330-331 waitpid(), 283, 296-300, 330-331, 349, 370, 380 флаги для, 297 warm fuzzy, 153 Watson, Gray, 581 wc, программа, 414-415 wcrtombO, 495 wcsrtombs(), 495 wcstombs(), 495 wctomb(), 495 wget, программа, 15 WINE, эмулятор, 590-591 wordexp(), 446-448, 458 wordfree(), 446-448 wprintfO, 495 write(), 91, 103-106, 117-119, 121, 141, 307-308, 322-324, 342, 349 X x, в правах доступа, 23 Xenix, файловая система, 230-231 XFS, файловая система, 231 xgettext, программа, 483, 490 xiafs, файловая система, 232 xinetd, программа, справочная страница, 362 XML (Extensible Markup Language), 565-566
Предметный указатель 637 всего файла, 501, 507 диапазон, 506 исключительные, 507 на запись, 500 на осуществление записи, 501, 504, 529-530 на чтение, 501, 504, 529-530 необязательные, 501-502,507-508,529-530 обязательные, 243, 502, 509-512, 529-530 получение, 505-507 разделяемые, 507-508 снятие, 505-508 буферный кэш, 117-119, 144, 422 буферы переполнение, 71, 79, 553 размер, 83-84, 155-157, 165, 254, 553, 560-561 стратегия обслуживания, 77-82 варьирующие макросы, 544-545 ввод/вывод, 91, 105-106 асинхронный, 356 блокирующий, 117 неблокирующий, 322-324 с последовательным доступом, 109 с произвольным доступом, 109, 113,121 синхронный, 118, 121, 243 стандартные функции для, 102 версия GNU, 259, 264-272, 274 владелец файла, см. пользователь владелец, категория пользователей, 21-22, 113-116,386 ID, см. UID базы данных, 195-198, 203 имена, 196,203,205,214 маски для, 149-150 пароли, 196 см. также владение владение, 21, 124 изменение, 158-159, 163, 165 маски для, 149-150 возвращаемые значения, 65, 291-292, 330 коды 131 и 132,292,295 отрицательные, 292 приведение типа, 69, 73-74 к void, 102 проверка, 69, 73-74, 102, 343 воспроизводимость, 534 временные отметки, 147,159-163 временные файлы, 33, 342, 415-423, 457 каталоги для, 132, 421 открытие, 419-421 время изменения индекса, 147, 206 изменение, 160 время изменения, 147-148, 206 изменение, 159-163, 165 получение, 513-514, 529-530 сортировка по, 206 форматирование, 176 время последнего доступа, 146-147, 206, 243 изменение, 159-163, 165 получение, 513-514, 529-530 форматирование, 176 время, 167,203 локальное, 169 разложенное, 169, 177, 203 разрешение, 512-515, 519, 529-530 текущее, 168 форматирование, 171-177, 462, 476 Всеобщее скоординированное время, см. UTC вытесняющая многозадачность, 283 Г гейзенберговские ошибки, 411 гибкие диски, 387, 509 гибкие ссылки, см. символические ссылки глифы, 493 глобализация, 461 Группа поддержки Unix, 43 группа, категория пользователей, 21-22, 113-116,385-386 ID, см. GID базы данных, 195, 198-201, 203 изменение, 158 имена, 199,203,205,214
638 Предметный указатель маски для, 149-150 пароли, 199 списки пользователей, 199 группы процесса, 302-305, 330-331 ID, см. PGID висячие, 303 лидеры, 302, 304, 330-331 отправка сигналов, 359-360 приоритетные, 303 установка, 304-305 фоновые, 303 д дампы ядра, 67, 298, 334,409,423-424,457, 536, даты, 167 текущая, 168 форматирование, 462, 476 двоичные данные, 113 двоичные деревья, 520-521 вставка в, 520-522 глубина, 520 обход, 521,525-529 поддеревья, 520 поиск в, 520-524 удаление в, 521, 528-529 узлы, 520, 525 указатели на, 521-524 двоичные исполняемые файлы, 23-24, 63-65, 89,236,330,395 действия (actions), см. действия сигналов (signal actions) действия сигналов, 334, 381 восстановление, 335 по умолчанию, 333-342, 350, 380-381 демон автоматического монтирования (automounter), 229 демоны, 273, 309 денежное форматирование, 462, 468-472, 497 деревья разбора, 554-555 дескрипторы файлов, 99-108, 121, 145, 164, 201,305 атрибуты, 317-325, 331 для открытых файлов, 278-280, 305, 320, 330, 420 дублирование, 319-320 закрытие, 312,424 копирование, 310-315, 324, 330-331 наименьшее доступное значение, 320 новый, 101-102 плохие, 95 получение, 253 разделяемые, 279, 330 утечки, 326 функции для, 120 десятичная точка, 463, 470 детерминизм, 431 Джонсон, Стив, 21 динамическая память, 33, 63, 67-89 выровненная, 499-500, 529-530 вычисление размера, 69, 75 доступ за пределами выделения, 71, 533-534, 575, 578, 585, 592-593 доступ после усечения, 73 изменение размера, 72-75, 85, 576-581 не освобожденная, 574-575, 592-593 неинициализироанное использование, 575, 585, 589-590, 599 освобождение, 70-72, 90 освобожденная, 70-71, 89, 448-449, 574-575,578,585 отладка, 574-592, 599 очистка, 73, 75, 411 первоначальное выделение, 68-70,389-390 трассировка, 575-576 утечки, 72, 73, 90, 188,415-416,429-431, 574-575, 585, 589 динамические структуры данных, 520 диспозиции, см. действия сигнала, по умолчанию дыры, ПО, 113,146 3 закон Хоара, 7 замещение процесса, 315 записываемый конец (канала), 305,308, 312
Предметный указатель 639 звуковой сигнал, 595 зомби, 295, 367 И идентификаторы, 67 имена путей, 124, 133 абсолютные, 254, 273 относительные, 26, 253 проверка правильности, 392 имена файлов, 23 базовое поведение программы по отношению к, 289 длина, 23, 95, 126-127, 138, 242 изменение, 130-131, 159 создание, 415-419 функции для, 120 именованные каналы, см. FIFO файлы именованные константы, 47, 100, 545-546, 548-549 использование enum для, 546-547 инварианты, 408 индексирование, 75, 79 индексные узлы, см. узлы инициализированные данные, 63 интервальные таймеры, см. таймеры интернационализация (il8n), 460, 497-498, 602 интерпретация, 24, 554-556 интерфейсы, 35 исполняемый код, 63-65, 89 К каналы, 27, 305, 330-331, 361-362 блокирующие, 322 буферирование, 307-308 двусторонние, 325-329 именованные, см. FIFO файлы неблокирующие, 322-324 нелинейные, 315, 330-331 пустой, 322 разрушенные, 97 синхронизация, 308 создание, 305-307 каталога права доступа, см. права доступа, каталог каталоги сообщений, 480, 483,487, 498 каталоги, 22-23,125-127,144, 164 абсолютный путь к, 254 для временных файлов, 421, 457 изменение, 253-254 маска для, 149-150 обход, 256-272, 273 перемещение, 133 родительский, 130 сведения о, 205 символические ссылки на, 133 системный корневой, 225-227, 272-273 создание, 134-136 текущее положение в, 143, 164 текущий корневой, 26, 164, 225, 272- 273, 278 текущий рабочий, 26-27, 36, 130, 233, 253, 272-273, 278 удаление, 132, 135 чтение, 136-143 квантование времени, 283, 417-418 код в стиле спагетти, 424 кодировки, 461, 493, 498 многобайтные, 495 самокорректирующие, 495-496 компиляторы, 554-556, 592-593 контрольные точки (breakpoints), 539, 548, 566-568, 598 внутренние макросы, 545-546 контрольные точки, 541, 598 криптография, 431, 437-438 критические разделы, 351, 375 куча, 64-65, 430 Л лазейки, 566-567 лексемы, 583 летнее время, см. DST липкие биты, 23, 254,395 для каталогов, 404-405 маска для, 149-150 Лицензия Caldera для старой Unix, 8, 610-611 ловушки, 335-336 локали, 461-462, 496-497
640 Предметный указатель категории, 462-464, 497 по умолчанию, 462 установка, 463-464 локализация (И On), 175, 460, 497-498 М магические числа, 24, 225 преобразование в печатные строки, 251 маска сигналов процесса, 278, 350-353, 361, 382, 427-428, 457 составление, 364-365 массивы в сравнении с деревьями, 519 вычисление числа элементов, 111 поиск, 190-195 сортировка, 181-190 место запятой, 473, 478 миникомпьютеры, 21, 25, 303 многопользовательские системы, 385-386 модель доступа к данным, 33, 36 монтирование, см. файловые системы, монтирование н наборы групп, 199, 387, 394, 404 изменение, 397, 404-405 получение, 389 число групп в, 389-390, 404-405 наборы сигналов, 352 наборы символов, 461, 493, 498 нарушение сегментации, 70 начальные числа, 431-432, 435-436, 458 неинициализированные данные, 64 нелокальные переходы, 424, 457, 573-674 номера индексов, 126-127, 130, 137, 146,153, 164,205 для корневого каталога, 227, 273 номера сигналов, 298, 335 номера устройств, 151, 225 О область кода (text), 480, 498 область подкачки,423 обобщение, 33 оболочки управления заданиями, 303, 361, 382-383 оболочки,602 разделение аргументов в, 37 сортировка переменных окружения, 55 обработка командной строки, 31 обработчики сигналов, 335, 339-351, 354-359, 381 восстановление, 347-348 на уровне оболочки, 336 переустановка, 340-341, 346 установка, 350, 364-365 функции, которые можно вызвать из, 348-349 обработчики, см. обработчики сигналов Общедоступная лицензия, см. GNU GPL Общий формат объектных файлов, см. COFF объединения в С, 541, 554-558, 598 вложенные, 556-558 объектные файлы сообщений, 492, 498 Обычные файловые системы на оборудовании х86, 228, 231, 273 окружение, 26-27, 52-61, 278 очистка, 54-55 операнды порядок, 39 размещение, 40 операторы проверки, 407-411, 457 оптимизации, 534, 539, 598 опции, 31,37-38 длинные, 31, 40, 46-52 имена, 38, 40 недействительные, 44-46 недокументированные, 558-55.9 отладки, 558-560, 570 размещение, 39-40 специфичные для производителя, 39, 49 Оригинальный С, 27-29, 35 параметры функций в, 592-593 программы GNU в, 30 см. также С, язык; Стандартный С отладка, 533-534
Предметный указатель 641 времени выполнения, 558-568, 598 компиляция для, 534-535, 539 макросы для, 544-545, 598 правила, 594-597, 599 отладочные символы, 534, 543, 598 отладочные файлы, 565-566 отладчики, 334, 409, 423, 533-535, 568-593, 595, 599 внутренние, 569 графические, 535, 598 уровня машинных кодов против уровня исходного кода, 535 относительный приоритет, 278, 283-285, 330 П память, 26, 66 динамически выделяемая, см. динамическая память использование, 33-34 перекрывающиеся области, 412 только для чтения, 65 установка, 411 папки,см. каталоги параметры, 41-42, 65 списки, 592-593 переводы, 480-493, 498 обновление, 493 подготовка, 488-489 создание, 489-493 тестирование, 487-488 переменные временные для отладки, 547 для важных условий, 544-545 журналирование, 565-566 локальные, 65 переменные окружения, 26-27, 52-54 для локалей, 462-464 для отладки, 560-562, 575, 580-581 добавление, 54-55 развертывание, 447, 458 с пустыми значениями,. 53 случайный порядок, 55 удаление, 54-55, 60 перенос строк,81 переносимость, 31, 34-35, 36, 51, 69, 79, 86-87, 140,329,544-545,602 переносимые объектные файлы, 490, 498 переносимые шаблоны объектов, 491 Переносимый интерфейс операционной системы, см. POSIX, стандарт переходные символы оболочки, 397 побитовые операторы, 247, 548 побочные эффекты, 411 поддержка родного языка, см. NLS подкачка по требованию, 395 подоболочки, 280 подстановка команд (command substitution), 447, 458 подсчет ссылок, 127,131, 146, 164 позиционные спецификаторы, 486-487 поиск бинарный, 190-195, 203, 414-415 в базах данных пользователей/групп, 198 линейный, 191, 203, 214, 408 постусловия,408 потоки,67 потребитель, 308, 331 пошаговое выполнение, 540, 598 права доступа, 36,124, 146, 148-154, 216 в восьмеричном виде, 114 изменение, 22, 115, 159, 163, 165 каталога, 23, 131 макрос для, 148 отказ в, 95 проверка, 385-386, 404-405 файла, 21, 113-116, 131 предсказуемые алгоритмы (mktempO), 417-418 предусловия,408 привилегированные операции, 397 приглашения, 13 принцип «всегда проверять возвращаемое значение», 102,343 принцип «проверять каждый вызов на ошибки», 33, 82 Принцип неопределенности, 410-411 приоритет, 283-285 пробуксовка, 72 проверки времени выполнения, 410, 457
642 Предметный указатель программы базовая структура, 92-93 дистрибутивы, 51 журналирование, 565-566 запуск, см. процессы имена, 38 недокументированные возможности в, 566-567 производственные версии, 534, 558- 559,560-561,579 сообщения в, 507-521, 497, 565-566 тестирование, 593-594, 599, 603 производитель, 307-308, 330-331 простота, 20, 25, 27, 33, 36 прототипы, 27-29, 36, 592-593 профилирование, 515 процессы, 25-27, 36, 63-65 ID, см. PID блокирование, 350, 515 висячие, 280 выполнение программ в, 285, 329 выход, 93 завершение, 291-295, 330, 334, 342, 364- 365,518,529-530 завершение, 381, 409, 457 записывающий, 307 опрос, 298 остановка, 295, 298, 334, 366-367, 381 порожденные, 26, 95, 277, 278, 280, 311- 315,326,330 завершившиеся, 295, 367-380 недетерминированный порядок, 315 приостановка, 362,366-367 продолжение при остановке, 334, 361, 367,378,379,381 родительские, 26, 277, 278, 280, 295, 312-315, 326, 329-330, 367-380 сбор сведений, 295-296 синхронизация, 283 создание, 277, 329 читающий, 307 прямые ссылки, 127-130, 133-134,164 на каталоги, 127, 130 на корневой каталог, 226 псевдослучайные числа, 431-439,456 псевдотерминалы (псевдо-tty), 25, 202, 229 разделители тысяч, 470, 472-474, 478 разделы, 124,164, 273 раскрывание метасимволов, см. раскрывание символов подстановки раскрывание подстановок, 439-440, 458 раскрывание тильды, 444, 447, 458 регистры, 65 регулярные выражения, 448-456, 458 основные, 448-449 расширенные, 448-449 режимы файлов, см. права доступа к файлам родительского процесса ID, см. PPID сборщики мусора, 33 сеансы, 302, 330-331,360 ID, 305 лидеры, 303 сегменты данных, 63-65 сегменты кода (text), 63-65, 89 сегменты стека, 65 секторы диска, 241 секции данных, 63-65 секции кода (text), 63-65 сетевые базы данных, 195 сигналы, 295-296, 330-331, 333-334,347-348 блокирование, 350, 351-352, 354, 361, 364,380-383 доступные под GNU/Linux, 336-337 завершения потомка, 382-383 игнорирование, 334, 335-336, 342, 359, 362,380-381 использование для IPC, 361-362, 382 ожидающие, 353, 359, 366-367, 380-383 отправка, 359-360, 381-382, 386 перехват, 335 прерывания, 25, 296, 303, 339, 349, 359
Предметный указатель 643 реального времени, 338 список поддерживаемых, 338 управления заданиями, 25, 296, 298, 303, 366, 382-383 сигнальные метки, 111 сигнальные часы (alarm clocks), 364-365, 514-515 символ возврата каретки, 79 символ конца строки, 53, 79, 413 символ перевода строки, 79 символические константы, 100 символические ссылки, 133-134, 144, 145, 154-157,164-165 владение, 158 временные отметки для, 164 маски для, 149-150 на каталоги, 133, 256 права доступа на, 159 создание, 154 уровни, 95 символы классы, 467, 496 порядок, 493 строчные против заглавных, 462, 467, 496 широкие, 494 символы валют, 462, 470, 472-473, 478-479 системная консоль, 25 системные вызовы, 6, 9, 26, 33, 91 косвенные, 141 неудачное завершение, 94, 120 повторно запускаемые, 342-346 прерываемые, 96, 342, 349 проверка ошибок, 105 стандарт POSIX для, 35 скорость, 33 слова множественная форма, 482, 498 порядок, 486-487, 498 служебные данные (метаданные), 23,124, 131,145-147,164-165 изменение времени, см. время изменения узла случайные числа, 431, 437-438, 456-458 смещения, 109, 503 совместимость со стандартами, 30 сокеты, 96-97,145 маска для, 149-150 сообщения об ошибках, 31, 33, 82, 98-99, 105, 120,423-424 диагностические идентификаторы для, 98-99 обработка, 44 функции отчета для, 97-98 сопроцессы, см. каналы, двусторонние сортировка, 181-190, 203, 423 по времени модификации, 206 указателей, 187 устойчивая, 184 состояния гонки, 131, 346-347, 349, 352, 363, 375, 382, 392, 417-418, 456-457, 517, 586 состояния регистра, 495 справочная страница, 205, 215 справочные страницы, 13, 595 ссылки, см. прямые ссылки стандартная ошибка, 5,27,36,100,121,145,201 отправка отладочных сообщений в, 543 Стандартный С, 36 1990 ISO, 8, 27-29, 34 1999 ISO, 8, 29, 34 main(), 292 realloc(), 74 remove(), 132 timet, тип, 167 variadic macros in, 544 завершение функций в, 293-295 программы GNU в, 31 см. также С, язык; Оригинальный С функции сигналов в, 335-338 широкие символы в, 494 элементы const в, 65 стандартный ввод, 5, 27, 36, 40, 100, 105, 121, 145,201,305,325,452,454 разделяемый двумя процессами, 279 стандартный вывод, 5, 27,36, 40,100, 121, 145,153,201,305,325 стандарты, 8
644 Предметный указатель статичные таблицы, 33 статус завершения, см. возвращаемые значения стек, 65, 86, 585 стиль форматирования кода K&R, 32 строки копирование, 84 пометка для перевода, 483 сравнение, 462, 464-468, 497 строки произвольной длины, 77-82, 90 структурирование, 264 структуры в С, 541 вложенные, 556-557 массивы, 554 размер, 69 суперблоки,225 суперпользователь, см. root схемы сортировки (collating sequences), 465 сценарии, 24 Т таблица файлов, 279 тайм-ауты, 516 таймеры, 514-518, 529-530 истечение, 515, 517 установка, 517 терминалы (tty), 25, 117, 144, 154, 201-203, 278, 366, 400-401 неблокирующие, 322 управляющие, 302-304 чтение данных из, 516 точки монтирования, 225, 273 трассировка стека, 538 тупики, 95, 506 У узлы (индексные узлы), 23, 125-127, 164, 224-225 число, 242, 273 указатели вычисление, 68 гарантированно действительные, 82 двойное освобождение, 71 зависшие, 71, 575 недействительные, 68, 73-75 общие, 68 объявление, 69 передача, 71 • сортировка, 187 установка в NULL, 71 уничтожение данных, 25 управление заданиями, 302-304, 330-331, 334, 366 условия, 546-547 журналирование, 565-566 использование переменных для, 544-545 устройства, 24-25, 36, 125, 144 блочные, 25, 144, 146, 151, 165, 236 возвратной петли, 232, 235 занятые, 95 маски для, 149-150 медленные, 343 символьные, 25, 144, 146, 151, 165, 236 типы,151 Ф файловые системы, 124,137,164, 224-232, 273 демонтирование, 224, 233 журналирование, 231, 273 монтирование, 141, 146, 224, 232-234, 273,391,505,508,529-530 отладка, 205 сведения о, 240 состояние занятости, 233 только для чтения, 97, 230-231, 235, 242-243 файлы журнала, 565-567, 575, 595 файлы, 20-25, 36 атрибуты, 317-325, 331 блокировка, 500-502, 529-530 восстановление из архива, 159, 161 закрытие, 101-103,280 запись в, 101, 103-106, 116-119, 121, 147, 159 копирование в себя, 108 маска для, 149-150
Предметный указатель 645 обычные, 144, 343 открытие, 96, 101-103, 119, 121 разделяемые, 103 размер, 95, 120, 146, 163, 256 сведения о, см. вспомогательные данные смещение внутри, 109, 117, 507 создание, 113-119, 121, 127-128 существующие, 95, 116 типы, 142-149, 153 макросы для, 151, 165 маски для, 149-150 удаление, 131-132 усечение, 116, 147 чтение, 101, 103-106, 117-118, 121 файлы, права доступа, см. права доступа к файлам флаги статуса файла, 319-321, 324, 331 флаги, 47, 246-247, 346, 548 преобразование в строку, 549-554 форматирование кода, 31, 607-608 форматы объектных файлов, 23 фреймы стека, 538 функции, 6, 9 вспомогательные, 548, 598 низкоуровневые, 84, 90 оболочки, 82 обратного вызова, 258, 293, 330, 525, 527 объявления, 29, 41 отладки, 550 рекурсивные, 65, 75, 264 соглашения по именованию, 120 X хэш-таблицы, 268 Ч часовые пояса, 178-180, 203 частные распределители памяти, 77 числа, 47 фуппирование цифр в, 470,472,474-475 форматирование, 462, 468-477, 497 читаемые конец (канала), 305, 308, 312 э электронные диски, 230, 422-423 элементы каталога, 126-127,130,133, 137-143, 164,205 длина, 242 сортировка, 188-191,211 типы файлов в, 142 чтение, 218 энтропийный пул, 437-438 Эпоха, 160,167,203,512 эры, 477-479
Содержание Предисловие , 5 Часть 1. Файлы и пользователи 19 Глава 1. Введение .. 20 1.1. Модель файловой системы Linux/Unix 20 1.1.1. Файлы и права доступа 21 1.1.2. Каталоги и имена файлов 22 1.1.3. Исполняемые файлы 23 1.1.4. Устройства 24 1.2. Модель процессов Linux/Unix 25 1.2.1. Каналы: сцепление процессов 27 1.3. Стандартный С против оригинального с 27 1.4. Почему программы GNU лучше 30 1.4.1. Проектирование программ 30 1.4.2. Поведение программы 31 1.4.3. Программирование на С 31 1.4.4. Вещи, которые делают программы GNU лучше , 32 1.4.5. Заключительные соображения по поводу «GNU Coding Standards» 34 1.5. Пересмотренная переносимость 34 1.6. Рекомендуемая литература 35 1.7. Резюме 36 Упражнения 36 Глава 2. Аргументы, опции и переменные окружения 37 2.1. Соглашения по опциям и аргументам 37 2.1.1. Соглашения POSIX 38 2.1.2. Длинные опции GNU 40 2.2. Базовая обработка командной строки 41 2.2.1. Программа echo V7 41 2.3. Разбор опций: getopt () и getopt_long () 43 2.3.1. Опции с одним символом ; 43 2.3.2. GNU getopt () и порядок опций 45 2.3.3. Длинные опции 46
Содержание 647 2.4. Переменные окружения 52 2.4.1. Функции управления окружением 53 2.4.2. Окружение в целом: environ ..54 2.4.3. GNU env .55 2.5. Резюме 61 Упражнения 61 Глава 3. Управление памятью на уровне пользователя 63 3.1. Адресное пространство Linux/Unix ....63 3.2. Выделение памяти ...67 3.2.1. Библиотечные вызовы: malloc (), calloc(), realloc(), free() 67 3.2.2. Копирование строк: strdup () 84 3.2.3. Системные вызовы: brk() и sbrk() 85 3.2.4. Вызовы ленивых программистов: alloca () 86 3.2.5. Исследование адресного пространства 87 3.3. Резюме '. 89 Упражнения 90 Глава 4. Файлы и файловый ввод/вывод 91 4.1. Введение в модель ввода/вывода Linux/Unix 91 4.2. Представление базовой структуры программы 92 4.3. Определение ошибок 93 4.3.1. Значения errno 94 4.3.2. Стиль сообщения об ошибках ....98 4.4. Ввод и вывод 99 4.4.1. Понятие о дескрипторах файлов 99 4.4.2. Открытие и закрытие файлов 101 4.4.3. Чтение и запись 103 4.4.4. Пример: Unix cat 106 4.5. Произвольный доступ: перемещения внутри файла ч. 108 4.6. Создание файлов 113 4.6.1. Определение начальных прав доступа к файлу 113 4.6.2. Создание файлов с помощью creat () 116 4.6.3. Возвращаясь корепО < 116 4.7. Форсирование записи данных на диск 119 4.8. Установка длины файла 120 4.9. Резюме 120 Упражнения 121
648 Содержание Глава 5. Каталоги и служебные данные файлов 123 5.1. Просмотр содержимого каталога 123 5.1.1. Определения , 124 5.1.2. Содержимое каталога 126 5.1.3. Прямые ссылки 127 5.1.4. Переименование файлов 130 5.1.5. Удаление файла 131 5.1.6. Символические ссылки ...133 5.2. Создание и удаление каталогов 134 5.3. Чтение каталогов 136 5.3.1. Базовое чтение каталогов ..137 5.3.2. Функции размещения каталогов BSD 143 5.4. Получение информации о файлах .' 143 5.4.1. Типы файлов Linux 144 5.4.2. Получение информации о файле 145 5.4.3. Только Linux: указание файлового времени повышенной точности 147 5.4.4. Определение типа файла 148 5.4.5. Работа с символическими ссылками 154 5.5. Смена владельца, прав доступа и времени изменения 158 5.5.1. Смена владельца файла: chownO, f chownO и lchown() 158 5.5.2. Изменение прав доступа: chmodOn fchmodO 159 5.5.3. Изменение временных отметок: utime () 159 5.5.4. Использование f chown () и f chmod () для обеспечения безопасности 163 5.6. Резюме 164 Упражнения 165 Глава 6. Общие библиотечные интерфейсы - часть 1 167 6.1. Времена и даты 167 6.1.1. Получение текущего времени: timeOndif ftime () 168 6.1.2. Разложение времени: gmtimeOn localtimeO 169 6.1.3. Форматирование даты и времени 171 6.1.4. Преобразование разложенного времени в time_t 177 6.1.5. Получение сведений о часовом поясе 178 6.2. Функции сортировки и поиска 181 6.2.1. Сортировка: qsort () 181 6.2.2. Бинарный поиск: bsearch() 190
Содержание 649 6.3. Имена пользователей и групп 195 6.3.1. База данных пользователей 196 6.3.2. База данных групп 198 6.4. Терминалы: isatty () 201 6.5. Рекомендуемая литература 202 6.6. Резюме 203 Упражнения 203 Глава 7. Соединяя все вместе: is ...205 7.1. Опции V7 Is , 205 7.2. Код V7 Is 206 7.3. Резюме 222 Упражнения > 223 Глава 8. Файловые системы и обходы каталогов 224 8.1. Монтирование и демонтирование файловых систем 224 8.1.1. Обзор основ 224 8.1.2. Обзор различных типов файловых систем 228 8.1.3. Монтирование файловых систем: mount 232 8.1.4. Демонтирование файловых систем: umount 233 8.2. Файлы для администрирования файловой системы 234 8.2.1. Использование опций монтирования 235 8.2.2. Работа со смонтированными файловыми системами: getmntent () 237 8.3. Получение сведений о файловой системе 240 8.3.1. Стиль POSIX: statvf s () и f statvf s () 240 8.3.2. Стиль Linux: statf s () и fstatf s () 248 8.4. Перемещение по иерархии файлов 252 8.4.1. Смена каталога: chdir() и fchdirO 253 8.4.2. Получение текущего каталога: getcwd () 254 8.4.3. Перемещение по иерархии: nf tw(} 256 8.5. Обход дерева файлов: GNU du 264 8.6. Изменение корневого каталога: chroot () 272 8.7. Резюме 273 Упражнения 274
650 Содержание Часть 2. Процессы, IPC и интернационализация , 275 Глава 9. Управление процессами и каналы 276 9.1. Создание и управление процессами 276 9.1.1. Создание процесса: fork() 277 9.1.2. Идентификация процесса: getpidO и getppidO 281 9.1.3. Установка приоритетов процесса: nice () 283 9.1.4. Запуск новой программы: семейство exec () : 285 9.1.5. Завершение процесса 291 9.1.6. Использование статуса завершения порожденного процесса ....295 9.2. Группы процессов 302 9.2.1. Обзор управления заданиями 302 9.2.2. Идентификация группы процессов: getpgrpO и getpgidO 303 9.2.3. Установка группы процесса: setpgid () nsetpgrpO .304 9.3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO .305 9.3.1. Каналы 305 9.3.2. Очереди FIFO 308 9.4. Управление дескрипторами файлов ; 310 9.4.1. Дублирование открытых файлов: dup() и dup2 () 310 9.4.2. Создание нелинейных конвейеров: /dev/f d/XX 315 9.4.3. Управление атрибутами файла: f cntl () 317 9.5. Пример: двусторонние каналы в gawk 325 9.6. Рекомендуемая литература 329 9.7. Резюме 329 Упражнения .331 Глава 10. Сигналы . 333 10.1. Введение 333 10.2. Действия сигналов 334 10.3. Стандартные сигналы С: signal () и raise () 335 10.3.1. Функция Signal () 335 10.3.2. Программная отправка сигналов: raise () 338 10.4. Обработчики сигналов в действии 339 10.4.1. Традиционные системы 339 10.4.2. BSD и GNU/Linux 341 10.4.3. Игнорирование сигналов 342 10.4.4. Системные вызовы, допускающие повторный запуск 342
Содержание 651 10.4.5. Состояния гонок и sig_atomic__t (ISO С) 346 10.4.6. Дополнительные предостережения 347 10.4.7. Наша история до настоящего времени, эпизод 1 348 10.5. API сигналов System V Release 3: sigset () и др 350 10.6. Сигналы POSIX : .351 10.6.1. Обнажение проблемы 351 10.6.2. Наборы сигналов: sigset__t и связанные функции 352 10.6.3. Управление маской сигналов: sigprocmask () идр 353 10.6.4. Перехват сигналов: sigactionO 354 10.6.5. Извлечение ожидающих сигналов: sigpending () 358 10.6.6. Создание возможности для прерывания функций: siginterrupt () 359 10.6.7. Передача сигналов: kill () и killpg () 359 10.6.8. Наша история до настоящего времени, эпизод II ;...361 10.7. Сигналы для межпроцессного взаимодействия 361 10.8. Важные сигналы специального назначения 364 10.8.1. Сигнальные часы: sleepO, alarm () и SI GAL ARM 364 10.8.2. Сигналы, управляющие заданиями , 366 10.8.3. Родительский надзор: три различные стратегии 367 10.9. Сигналы, передающиеся через fork () и exec () 380 10.10. Резюме 1 381 Упражнения 383 Глава 11. Права доступа и ID пользователей и групп 385 11.1. Проверка прав доступа 385 11.1.1. Действительные и эффективные ID 386 11.1.2. Биты Setuid и Setgid 387 11.2. Получение ID пользователя и группы 388 11.3. Проверка для действительного пользователя: access () 391 11.4. Проверка для эффективного пользователя: euidaccessO (GLIBC) 393 11.5. Установка дополнительных битов доступа для каталогов 393 11.5.1. Группа по умолчанию для новых файлов и каталогов 393 11.5.2. Каталоги и «липкий» бит 395 11.6. Установка действительных и эффективных ID 396 11.6.1. Изменение набора групп 396 11.6.2. Изменение действительного и эффективного ID 397 11.6.3. Использование битов setuid и setgid 399
652 Содержание 11.7. Работа со всеми тремя ID: getresuidO и setresuidO (Linux) 402 11.8. Пересечение минного поля безопасности: setuid root 403 11.9. Рекомендуемая литература 404 11.10. Резюме 404 Упражнения , 405 Глава 12. Общие библиотечные интерфейсы - часть 2 407 12.1. Операторы проверки: assert () ..407 12.2. Низкоуровневая память: функции memXXX () 411 12.2.1. Заполнение памяти: memset () 411 12.2.2. Копирование памяти: rnemcpy (), mernmove () и memccpy () ..412 12.2.3. Сравнение блоков памяти: memcmpO 413 12.2.4. Поиск байта с данным значением: memchr () 414 12.3. Временные файлы г 415 12.3.1. Создание временных имен файлов (плохо) 415 12.3.2. Создание и открывание временных файлов (хорошо) 419 12.3.3. Использование переменной окружения TMPDIR 421 12.4. Совершение самоубийства: abort () 423 12.5. Нелокальные переходы 424 12.5.1. Использование стандартных функций: setjmpO и longjmpO 425 12.5.2. Обработка масок сигналов: sigset jmp() и siglongjmp() 427 12.5.3. Важные предостережения 427 12.6. Псевдослучайные числа 431 12.6.1. Стандартный С: rand() и srand() 432 12.6.2. Функции POSIX: randomO и srandomO 435 12.6.3. Особые файлы /dev/random и /dev/urandom 437 12.7. Расширения метасимволов 439 12.7.1. Простое сопоставление с шаблоном: ?nxnatch() 439 12.7.2. Раскрытие имени файла: glob () и globf гее () 441 12.7.3. Разворачивание слов оболочкой: wordexpO и wordfreeO 447 12.8. Регулярные выражения 448 12.9. Рекомендуемая литература 456 12.10. Резюме 457 Упражнения 458
Содержание 653 Глава 13. Интернационализация и локализация 460 13.1. Введение , 460 13.2. Локали и библиотека С 461 13.2.1. Категории локалей и переменные окружения 462 13.2.2. Установка локали: setlocale () 463 13.2.3. Сравнение строк: strcoll () и strxf rm() 464 13.2.4. Числовое и денежное низкоуровневое форматирование: localeconv() 468 13.2.5. Высокоуровневое числовое и денежное форматирование: strfmonO nprintf () 471 13.2.6. Пример: форматирование числовых значений в gawk 474 13.2.7. Форматирование значений даты и времени: ctime () и strf time () 476 13.2.8. Другие данные локали: nl_langinf о () 477 13.3. Динамический перевод сообщений программ .480 13.3.1. Установка текстового домена: textdomain () 480 13.3.2. Перевод сообщений: gettext () 481 13.3.3. Работа с множественными числами: ngettext () 482 13.3.4. Упрощение использования gettext () 483 13.3.5. Перестановка порядка слов с помощью printf () 486 13.3.6. Тестирование переводов в персональном каталоге 487 13.3.7. Подготовка интернационализированных программ 488 13.3.8. Создание переводов 489 13.4. Не могли бы вы произнести это для меня по буквам? 493 13.4.1. Широкие символы 494 13.4.2. Представления многобайтных символов 495 13.4.3. Языки 496 13.4.4. Заключение 497 13.5. Рекомендуемая литература .497 13.6. Резюме '. 497 Упражнения 498 Глава 14. Расширенные интерфейсы 499 14.1. Выделение выровненной памяти: posix_memalign () и memalign () 499 14.2. Блокировка файлов 500 14.2.1. Концепции блокировки файлов 500 14.2.2. Блокировка POSIX: f cntl () и lockf () ...502
654 Содержание 14.2.3. Блокирование BSD: f lock() 507 14.2.4. Обязательная блокировка ...509 14.3. Более точное время 512 14.3.1. Время в микросекундах: gettimeofday() 512 14.3.2. Файловое время в микросекундах: utimes () 513 14.3.3. Интервальные таймеры: setitimer () и getitimer () ....514 14.3.4. Более точные паузы: nanosleepO ¦ 518 14.4. Расширенный поиск с помощью двоичных деревьев 519 14.4.1. Введение в двоичные деревья 519 14.4.2. Функции управления деревьями 521 14.4.3. Ввод элемента в дерево: tsearch () 522 14.4.4. Поиск по дереву и использование возвращенного указателя: tfind () и tsearch () 523 14.4.5. Обход дерева: twalk() 525 14.4.6. Удаление вершины дерева и удаление дерева: tdeleteO и tdestroyO 528 14.5. Резюме 529 Упражнения 530 Часть 3. Отладка и заключительный проект 532 Глава 15. Отладка 532 Глава 16. Проект, связывающий все воедино 532 Глава 15. Отладка . 533 15.1. Сначала главное 534 15.2. Компиляция для отладки 534 15.3. Основы GDB .535 15.3.1. Запуск GDB 536 15.3.2. Установка контрольных точек, пошаговое выполнение и отслеживаемые точки 539 15.4. Программирование для отладки 542 15.4.1. Код отладки времени компилирования 542 15.4.2. Отлаживаемый код времени исполнения 558 15.5. Отладочные инструменты 568 15.5.1. Библиотека dbug - усовершенствованный printf () 569 15.5.2. Отладчики выделения памяти 574 15.5.3. Современная lint 592 15.6. Тестирование программ 593 15.7. Правила отладки 595
Содержание 655 15.8. Рекомендуемая литература 597 15.9. Резюме 598 Упражнения 599 Глава 16. Проект, связывающий все воедино 601 16.1. Описание проекта 601 16.2. Рекомендуемая литература 603 Часть 4. Приложения 604 Приложение А. Научитесь программированию за десять лет 605 Приложение В. Старая лицензия Unix Caldera 610 Приложение С. Общедоступная лицензия GNU 612 Приложение А. Научитесь программированию за десять лет 605 Почему каждый в такой спешке? 605 Научитесь программированию за десять лет 606 Ссылки 608 Ответы '. 608 Сноски 609 Приложение В. Лицензия Caldera для старой Unix .' . 610 Приложение С. Общедоступная лицензия GNU 612 Преамбула 612 Права и обязанности при копировании, распространении и модификации 613 Отказ от гарантий 617 КОНЕЦ ОПИСАНИЯ ПРАВ И ОБЯЗАННОСТЕЙ 618 Как применить эти условия договора к своим новым программам 618 Пример использования 619 Предметный указатель 621