Linux программирование в примерах

Роббинс Арнольд

В книге рассмотрены вопросы, связанные с программированием под Linux: файловый ввод/вывод, метаданные файлов, основы управления памятью, процессы и сигналы, пользователи и группы, вопросы интернационализации и локализации, сортировка, поиск и многие другие. Много внимания уделено средствам отладки, доступным под GNU Linux. Все темы иллюстрируются примерами кода, взятого из V7 UNIX и GNU. Эта книга может быть полезна любому, кто интересуется программированием под Linux.

Предисловие

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

Рассматривая конкретные программы, вы не только видите, как использовать Linux API, но можете также исследовать реальные проблемы (производительности, переносимости, устойчивости), которые возникают при написании программного обеспечения.

Хотя книга называется

Программирование под Linux на примерах

, все, что мы рассматриваем, относится также к современным системам Unix, если не отмечено противное. Обычно мы используем термин «Linux» для обозначения ядра Linux, a «GNU/Linux» для обозначения всей системы (ядра, библиотек, инструментов). Часто также мы говорим «Linux», когда имеем в виду и Linux, GNU/Linux и Unix; если что-то является специфичным для той или иной системы, мы отмечаем это явным образом.

Данная книга предназначена для лиц, разбирающихся в программировании и знакомых с основами С, по крайней мере на уровне книги Кернигана и Ричи

Программирование на языке С

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

Часть 1

Файлы и пользователи

Глава 1

Введение

Если есть одна фраза, резюмирующая важнейшие понятия GNU/Linux (а следовательно, и Unix), это «файлы и процессы». В данной главе мы рассмотрим модели файлов и процессов в Linux. Их важно понять, потому что почти все системные вызовы имеют отношение к изменению какого-либо атрибута или части состояния файла или процесса.

Далее, поскольку мы будем изучать код в обеих стилях, мы кратко рассмотрим главные различия между стандартным С 1990 г. и первоначальным С. Наконец, мы довольно подробно обсудим то, что делает GNU-программы «лучше» — принципы программирования, использование которых в коде мы увидим.

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

1.1. Модель файловой системы Linux/Unix

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

Поиск простоты направлялся двумя факторами. С технической точки зрения, первоначальные мини-компьютеры PDP-11, на которых разрабатывалась Unix, имели маленькое адресное пространство: 64 килобайта на меньших системах, 64 Кб кода и 64 Кб данных на больших. Эти ограничения относились не только к обычным программам (так называемому коду

уровня пользователя

), но и к самой операционной системе (коду

уровня ядра

). Поэтому не только «Маленький — значит красивый» в эстетическом смысле, но «Маленький — значит красивый», потому что не было другого выбора!

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

1.1.1. Файлы и права доступа

Файловая модель Unix проста, как фраза: файл — это линейный поток байтов. Точка. Операционная система не накладывает на файлы никаких предопределенных структур, ни фиксированных или переменных размеров записей, ни индексированных файлов, ничего. Интерпретация содержимого файла целиком оставлена приложению. (Это не совсем верно, как мы вскоре увидим, но для начала достаточно близко к истине.)

Если у вас есть файл, вы можете сделать с данными в файле три вещи: прочитать, записать или исполнить их.

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

jane

является начальником пользователя

fred

, и

jane

не хочет, чтобы

fred

прочел последние результаты аттестации.

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

владелец

файла;

группа

пользователей, связанная с данным файлом (вскоре будет пояснено); и

остальные пользователи

. Для каждой из этих категорий

каждый

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

ls -l

':

$ ls -l progex.texi

1.1.2. Каталоги и имена файлов

Раз у вас есть файл, нужно где-то его хранить. В этом назначение

каталога

(известного в системах Windows или Apple Macintosh под названием «папка»). Каталог является особой разновидностью файла, связывающего имена файлов с метаданными, известными как

узлы

(inodes). Каталоги являются особыми, поскольку их может обновлять лишь операционная система путем описанных в главе 4, «Файлы и файловый ввод-вывод», системных вызовов. Они особые также потому, что операционная система предписывает формат элементов каталога.

Имена файлов могут содержать любой 8-битный байт, за исключением символа '

/

' (прямой косой черты) и ASCII символа NUL, все биты которого содержат 0. Ранние Unix- системы ограничивали имена 14 байтами; современные системы допускают отдельные имена файлов вплоть до 255 байтов.

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

о

файле, а не данные самого файла, отсюда термин

метаданные

.

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

удалять

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

1.1.3. Исполняемые файлы

Помните, мы говорили, что операционная система на накладывает структуру на файлы? Мы уже видели, что это было невинной ложью относительно каталогов. Это же относится к двоичным исполняемым файлам. Чтобы запустить программу, ядро должно знать, какая часть файла представляет инструкции (код), а какая — данные. Это ведет к понятию формата

объектного файла

, которое определяет, как эти данные располагаются внутри файла на диске.

Хотя ядро запустит лишь файлы, имеющие соответствующий формат, создание таких файлов задача утилит режима пользователя. Компилятор с языка программирования (такого как Ada, Fortran, С или С++) создает объектные файлы, а затем компоновщик или загрузчик (обычно с именем

ld

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

root

).

Поскольку компилятор, ассемблер и загрузчик являются инструментами режима пользователя, изменить со временем по мере необходимости форматы объектных файлов (сравнительно) просто; надо только «научить» ядро новому формату, и он может быть использован. Часть ядра, загружающая исполняемые файлы, относительно невелика, и это не является невозможной задачей. Поэтому форматы файлов Unix развиваются с течением времени. Первоначальный формат был известен как a.out (Assembler OUTput — вывод сборщика). Следующий формат, до сих пор использующийся в некоторых коммерческих системах, известен как COFF (Common Object File Format — общий формат объектных файлов), а современный, наиболее широко использующийся формат — ELF (Extensible Linking Format — открытый формат компоновки). Современные системы GNU/Linux используют ELF.

Ядро распознает, что исполняемый файл содержит двоичный объектный код, проверяя первые несколько байтов файла на предмет совпадения со специальными

магическими числами

. Это последовательности двух или четырех байтов, которые ядро распознает в качестве специальных. Для обратной совместимости современные Unix-системы распознают несколько форматов. Файлы ELF начинаются с четырех символов «

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

1.1.4. Устройства

Одним из самых замечательных новшеств Unix было объединение файлового ввода- вывода и ввода-вывода от устройств.

[14]

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

никаких частных случаев для кода пользователя

.

В повседневной практике, в частности, на уровне оболочки, часто появляются два устройства:

/dev/null

и

/dev/tty

.

/dev/null

является «битоприемником». Все данные, посылаемые

/dev/null

, уничтожаются операционной системой, а все попытки прочесть отсюда немедленно возвращают конец файла (EOF).

/dev/tty

является текущим управляющим терминалом процесса — тем, который он слушает, когда пользователь набирает символ прерывания (обычно CTRL-C) или выполняет управление заданием (CTRL-Z).

Системы GNU/Linux и многие современные системы Unix предоставляют устройства

/dev/stdin

,

/dev/stdout

и

/dev/stderr

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

1.2. Модель процессов Linux/Unix

Процесс является работающей программой.

[15]

Процесс имеет следующие атрибуты:

уникальный идентификатор процесса (PID);

• родительский процесс (с соответствующим идентификатором, PPID);

• идентификаторы прав доступа (UID, GID, набор групп и т.д.);

• отдельное от всех других процессов адресное пространство;

1.2.1. Каналы: сцепление процессов

Без сомнения, вам приходилось использовать конструкцию ('

|

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

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

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

1.3. Стандартный С против оригинального С

В течение многих лет определение С де-факто можно было найти в первом издании книги Брайана Кернигана и Денниса Ричи «Язык программирования С» (Brian Kernighan & Dennis Ritchie,

The С Programming Language

). Эта книга описала С, как он существовал для Unix и на системах, на которые его перенесли разработчики лаборатории Bell Labs. На протяжении данной книги мы называем его как «оригинальный С», хотя обычным является также название «С Кернигана и Ричи» («K&R С»), по именам двух авторов книги. (Деннис Ричи разработал и реализовал С.)

Стандарт ISO С 1990 г.

[17]

формализовал определения языка, включая функции библиотеки С (такие, как

printf()

и

fopen()

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

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

Объявление

extern int myfunc(struct my_struct *a,

1.4. Почему программы GNU лучше

Что делает программу GNU программой GNU?

[19]

Что делает программное обеспечение GNU «лучше» по сравнению с другим (платным или бесплатным) программным обеспечением? Наиболее очевидной разницей является общедоступная лицензия (General Public License — 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++ теперь тоже вполне обычен. Примером широко используемого пакета GNU, написанного на С++, является

groff

(GNU

troff

). Наш опыт говорит, что с GCC, поддерживающим С++, установка

groff

не представляет сложности.

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

[20]

. Но поскольку ядро еще не завершено, и пользователи используют инструменты GNU на не-GNU системах, переносимость желательна, но не является первостепенной задачей. Стандарт рекомендует для достижения переносимости между различными системами Unix использовать Autoconf.

1.4.2. Поведение программы

Глава 4

GNU Coding Standards

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

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

Утилиты GNU должны использовать для обработки командной строки функцию

getopt_long()

. Эта функция предусматривает разбор аргументов командной строки как для опций в стиле традиционного Unix ('

gawk -F:...

'), так и для длинных опций в стиле GNU ('

gawk --field-separator=:...

'). Все программы должны предусматривать опции

--help

и

--version

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

В качестве простого, но очевидного примера,

--verbose

пишется точно таким же способом во всех GNU-программах. Сравните это с

-v

,

-V

,

-d

и т.д. во многих других программах Unix. Большая часть главы 2, «Аргументы, опции и окружение», с. 23, посвящена механике разбора аргументов и опций.

1.4.3. Программирование на С

Наиболее привлекательной частью

GNU Coding Standards

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

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

gawk

, который мы поддерживаем, вы увидите, что он форматирован в стандартном стиле K&R (стиль расположения кода, использованный в обоих изданиях книги Кернигана и Ричи). Но это единственное отклонение в

gawk

от этой части стандартов кодирования.

Тем не менее, хотя нам и не нравится стиль FSF

[21]

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

GNU Coding Standards

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

indent

от GNU или

cb

от Unix.)

Что мы сочли важным в главе о написании кода на С, это то, что эти советы хороши для

любого

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

1.4.4. Вещи, которые делают программы GNU лучше

Теперь мы рассмотрим раздел, озаглавленный «

Написание надежных программ

», в главе 4 «

Поведение программ для всех программ

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

Это правило, возможно, единственное наиболее важное в проектировании программного обеспечения GNU —

никаких произвольных ограничений

. Все инструменты GNU должны быть способны обрабатывать произвольные объемы данных.

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

gawk

, регулярно запускающий программу

awk

для более чем 650 000 файлов (нет, это не опечатка) для сбора статистики,

gawk

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

awk

.

[22]

Глава 2

Аргументы, опции и переменные окружения

Первой задачей любой программы обычно является интерпретация опций и аргументов командной строки. Данная глава рассматривает, как программы С (и С++) получают аргументы своей командной строки, описывает стандартные процедуры для разбора опций и бросает взгляд на переменные окружения.

2.1. Соглашения по опциям и аргументам

У слова

аргументы

есть два значения. Более техническим определением является «все 'слова' в командной строке». Например:

$ ls main.с opts.с process.с

Здесь пользователь напечатал четыре «слова». Все четыре слова сделаны доступными программе в качестве ее аргументов

[27]

.

Второе определение более неформальное: аргументами являются все слова командной строки,

за исключением

имени команды. По умолчанию, оболочки Unix отделяют аргументы друг от друга

разделителями

(пробелами или символами TAB). Кавычки позволяют включать в аргументы разделитель:

$ echo here are lots of spaces

2.1.1. Соглашения POSIX

Стандарт POSIX описывает ряд соглашений, которых придерживаются удовлетворяющие стандарту программы. Никто от вас не требует, чтобы ваши программы удовлетворяли этим стандартам, но это хорошая мысль сделать так: пользователи Linux и Unix по всему миру понимают и используют эти соглашения, и если вы не будете им следовать, ваши пользователи будут несчастны. (Или у вас вообще не будет пользователей!) Более того, функции, которые мы обсуждаем далее в этой главе, освобождают вас от бремени ручной реализации этих соглашений для каждой программы, которую вы пишете. Вот эти правила, перефразированные из стандарта:

1. В имени программы должно быть не менее двух и не более девяти символов.

2. Имена программ должны содержать лишь строчные символы и цифры.

3. Имя опции должно быть простым буквенно-цифровым символом. Опции с множеством цифр не должны допускаться. Для производителей, реализующих утилиты POSIX, опция

-W

зарезервирована для специфичных для производителя опций.

4. Все опции должны начинаться с символа '

-

'.

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

.

2.2. Базовая обработка командной строки

Программа на С получает доступ к своим аргументам командной строки через параметры

argc

и

argv

. Параметр

argc

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

main()

, отличающихся способом объявления

argc

:

int main(int argc, char *argv[])  int main(int argc, char **argv)

{                                 {

...                                ...

}                                 }

2.2.1. Программа

echo

V7

Возможно, простейшим примером обработки командной строки является программа V7

echo,

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

-n

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

[28]

:

1  #include <stdio.h>

2

3  main(argc, argv) /*int main(int argc, char **argv)*/

4  int argc;

2.3. Разбор опций:

getopt()

и

getopt_long()

Примерно в 1980-х группа поддержки Unix для System III в AT&T заметила, что каждая программа Unix использовала для разбора аргументов свои собственные методики. Чтобы облегчить работу пользователей и программистов, они разработали большинство из перечисленных ранее соглашений. (Хотя изложение в System III справки для

intro

(1) значительно менее формально, чем в стандарте 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;

2.3.2. GNU

getopt()

и порядок опций

Стандартная функция

getopt()

прекращает поиск опций, как только встречает аргумент командной строки, который не начинается с GNU

getopt()

отличается: она просматривает в поисках опций всю командную строку. По мере продвижения она переставляет элементы

argv

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

argv[optind]

до

argv[argc-1]

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

--

' завершает сканирование опций.

Вы можете изменить поведение по умолчанию, использовав в

optstring

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

optstring[0] == '+'

GNU

getopt()

ведет себя, как стандартная

getopt()

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

POSIXLY_CORRECT

.

optstring[0] == '-'

2.3.3. Длинные опции

Функция

getopt_long()

осуществляет разбор длинных опций в описанном ранее виде. Дополнительная процедура

getopt_long_only()

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

-

'. В остальных случаях обе функции работают точно так же, как более простая функция GNU

getopt()

. (Для краткости, везде, где мы говорим «

getopt_long()

», можно было бы сказать «

getopt_long()

и

getopt_long_only()

».) Вот объявления функций из справки getopt(3) GNU/Linux:

#include <getopt.h> /* GLIBC */

int getopt_long(int argc, char *const argv[],

 const char *optstring,

2.3.3.1. Таблица длинных опций

Длинные опции описываются с помощью массива структур

struct option

. Структура

struct option

определена в

<getopt.h>

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

struct option {

 const char *name;

 int has_arg;

 int *flag;

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

-Wfile=myfile

тем же, что

--file=myfile

. Использование точки с запятой позволяет программе использовать при желании

-W

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

gawk

использует ее для совместимости с POSIX.)

2.3.3 3. Сводка возвращаемых значений

getopt_long()

Теперь должно быть ясно, что

getopt_long()

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

Таблица 2.2

. Возвращаемые значения

getopt_long()

2.3.3.4. GNU

getopt()

или

getopt_long()

в программах пользователей

Вы можете захотеть использовать в своих программах GNU

getopt()

или

getopt_long()

и заставить их работать на не-Linux системах/ Это нормально; просто скопируйте исходные файлы из программы GNU или из CVS архива библиотеки С GNU (GLIBC)

[30]

. Исходные файлы

getopt.h

,

getopt.с

и

getopt1.c

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

COPYING.LIB

наряду с файлами

getopt.h

,

getopt.с

и

getopt1.с

.

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

getopt_long()

, используйте '

#include <getopt.h>

', а не '

#include "getopt.h"

'. Затем, при компилировании, добавьте к командной строке компилятора С

-I

. Таким способом сначала будет найдена локальная копия заголовочного файла.

Вы можете поинтересоваться: «Вот так, я уже использую GNU/Linux. Почему я должен включать

getopt_long()

в свой исполняемый модуль, увеличивая его размер, если процедура уже находится в библиотеке С?» Это хороший вопрос. Однако, здесь не о чем беспокоиться. Исходный код построен так, что если он компилируется на системе, которая использует GLIBC, откомпилированные файлы не будут содержать никакого кода! Вот подтверждение на нашей системе:

$ uname -а /* Показать имя и тип системы */

Linux example 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux

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

Окружение

представляет собой набор пар вида '

имя = значение

' для каждой программы. Эти пары называются

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

. Каждое имя состоит от одной до любого числа буквенно-цифровых символов или символов подчеркивания ('

_

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

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

POSIXLY_CORRECT

, многие программы запрещают расширения или историческое поведение, которые несовместимы со стандартом POSIX.

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

Конечно, недостатком использования переменных окружения является то, что они могут молча изменять поведение программы. Джим Мейеринг (Jim Meyering), сопроводитель Coreutils, выразил это таким образом:

2.4.1. Функции управления окружением

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

#include <stdlib.h>

char *getenv(const char *name);

/* ISO С: Получить переменную

2.4.2. Окружение в целом:

environ

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

Внешняя переменная

environ

предоставляет доступ таким же способом, как

argv

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

environ

намеренно не объявлена ни в одном стандартном заголовочном файле (Это, кажется, прослеживается из исторической практики.) Вот объявление:

extern char **environ; /* Смотрите, нет заголовочного файла POSIX */

Как и в

argv

, завершающим элементом

environ

является

NULL

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

argc

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

/* ch02-printenv.c --- Распечатать окружение. */

2.4.3. GNU

env

Чтобы завершить главу, рассмотрим GNU версию команды

env

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

getopt_long()

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

$ env --help

Usage: env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]

/* Устанавливает соответствующее VALUE для каждого NAME и запускает COMMAND */

-i, --ignore-environment /* запустить с пустым окружением */

Глава 3

Управление памятью на уровне пользователя

Без памяти для хранения данных программа не может выполнить никакую работу (Или, скорее, невозможно выполнить никакую

полезную

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

динамически выделяемой памяти

— памяти, выделяемой в ходе исполнения, а не при компиляции. Вот как вводится в действие принцип GNU «никаких произвольных ограничений».

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

3.1. Адресное пространство Linux/Unix

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

Код

Часто называемая

сегментом текста

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

Инициализированные данные

Статически выделенные и глобальные данные, которые инициализированы ненулевыми значениями, находятся в

сегменте данных

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

секцией данных

.

3.2. Выделение памяти

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

new

и

delete

.

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

3.2.1. Библиотечные вызовы:

malloc()

,

calloc()

,

realloc()

,

free()

Динамическую память выделяют с помощью функций

malloc()

или

calloc()

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

realloc()

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

free()

.

Отладка использования динамической памяти сама по себе является важной темой. Инструменты для этой цели мы обсудим в разделе 15.5.2 «Отладчики выделения памяти».

3.2.1.1. Исследование подробностей на языке С

Вот объявления функций из темы справки GNU/Linux

malloc

(3):

#include <stdlib.h> /* ISO С */

void *calloc(size_t nmemb, size_t size);

 /* Выделить и инициализировать нулями */

3.2.1.2. Начальное выделение памяти:

malloc()

Сначала память выделяется с помощью

malloc()

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

NULL

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

errno

. (errno является специальной переменной, которую системные вызовы и библиотечные функции устанавливают для указания произошедшей ошибки. Она описывается в разделе 4.3 «Определение ошибок».) Например, предположим, что мы хотим выделить переменное число некоторых структур. Код выглядит примерно так:

struct coord { /* 3D координаты */

 int x, y, z;

} *coordinates;

unsigned int count; /* сколько нам нужно */

3.2.1.3. Освобождение памяти:

free()

Когда вы завершили использование памяти, «верните ее обратно», используя функцию

free()

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

free()

пустой указатель:

free(coordinates);

coordinates = NULL; /* не требуется, но хорошая мысль */

После вызова f

ree(coordinates)

доступ к памяти, на которую указывает

coordinates

,

запрещен

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

free()

:

Доступ к освобожденной памяти

3.2.1.4. Изменение размера:

realloc()

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

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

realloc()

. Продолжая пример с

coordinates

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

int new_count;

size_t new_amount;

struct coord *newcoords; /* установить, например: */

3.2.2. Копирование строк:

strdup()

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

strdup()

:

#include <string.h>

/* strdup --- выделить память с malloc() и скопировать строку */

char *strdup(const char *str) {

3.2.3. Системные вызовы:

brk()

и

sbrk()

Четыре функции, которые мы рассмотрели (

malloc()

,

calloc()

,

realloc()

и

free()

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

На Unix-системах стандартные функции реализованы поверх двух дополнительных, очень примитивных процедур, которые непосредственно изменяют размер адресного пространства процесса. Мы представляем их здесь, чтобы помочь вам понять, как работают GNU/Linux и Unix (снова «под капотом»); крайне маловероятно, что вам когда-нибудь понадобится использовать эти функции в обычных программах. Они определены следующим образом:

#include <unistd.h> /* Обычный */

#include <malloc.h> /* Необходим для систем GLIBC 2 */

3.2.4. Вызовы ленивых программистов:

alloca()

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

alloca()

; она объявлена следующим образом:

/* Заголовок в GNU/Linux, возможно, не на всех Unix-системах */

#include <alloca.h> /* Обычный */

void *alloca(size_t size);

3.3. Резюме

• У каждой программы Linux и (Unix) есть различные области памяти. Они хранятся в разных частях файла исполняемой программы на диске. Некоторые из секций загружаются при запуске программы в одну и ту же область памяти. Все запушенные экземпляры одной и той же программы разделяют исполняемый код (сегмент текста). Программа

size

показывает размеры различных областей переместимых объектных файлов и полностью скомпонованных исполняемых файлов.

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

NULL

.

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

malloc()

,

calloc()

или

realloc()

. Память освобождается с помощью

free()

. (Хотя с помощью

realloc()

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

• Необходимо предпринять чрезвычайные меры осторожности в следующих случаях

 • освобождать лишь память, выделенную с помощью соответствующих процедур,

Упражнения

1. Начав со структуры —

struct line {

 size_t buflen;

 char *buf;

 FILE* fp;

Глава 4

Файлы и файловый ввод/вывод

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

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 */

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

Наша версия cat следует структуре, которая обычно является полезной. Первая часть начинается с комментариев, заголовочных файлов, объявлений и функции main():

1  /*

2   * ch04-cat.c --- Демонстрация open(), read(), write(), close(),

3   * errno и strerror().

4   */

4.3. Определение ошибок

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

всегда

проверять успешность завершения каждой операции.

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

int result;

result = some_system_call(param1, param2);

if (result < 0) {

4.3.1. Значения

errno

Стандарт POSIX 2001 определяет большое число возможных значений для errno. Многие из них относятся к сетям, IPC или другим специальным задачам. Справочная страница для каждого системного вызова описывает возможные значения

errno

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

Таблица 4.1

. Значения GLIBC для

errno

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

intro

(2) и

errno

(2) для локальной системы.

4.3.2. Стиль сообщения об ошибках

Для использования в сообщениях об ошибках С предоставляет несколько специальных макросов. Наиболее широкоупотребительными являются

__FILE__

и

__LINE__

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

__func__

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

if (some_system_call(param1, param2) < 0) {

 fprintf(stderr, "%s: %s (%s %d): some_system_call(%d, %d) failed: %s\n",

  argv[0], __func__, __FILE__, __LINE__,

  param1, param2, strerror(errno));

4.4. Ввод и вывод

Все операции Linux по вводу/выводу осуществляются посредством

дескрипторов файлов

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

4.4.1. Понятие о дескрипторах файлов

Дескриптор файла

является целым значением. Действительные дескрипторы файлов начинаются с 0 и растут до некоторого установленного системой предела. Эти целые фактически являются индексами таблицы открытых файлов для каждого процесса (Таблица поддерживается внутри операционной системы; она недоступна запущенным программам.) В большинстве современных систем размеры таблиц большие. Команда '

ulimit -n

' печатает это значение:

$ ulimit -n

1024

Из С максимальное число открытых файлов возвращается функцией

getdtablesize()

(получить размер таблицы дескрипторов):

#include <unistd.h> /* Обычный */

4.4.2. Открытие и закрытие файлов

Новые дескрипторы файлов получают (наряду с другими источниками) в результате системного вызова

open()

. Этот системный вызов открывает файл для чтения или записи и возвращает новый дескриптор файла для последующих операций с этим файлом. Мы видели объявление раньше:

#include <sys/types.h> /* POSIX */

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

4.4.2.1. Отображение переменных

FILE*

на дескрипторы файлов

Стандартные библиотечные функции ввода/вывода и переменные

FILE*

из

<stdio.h>

, такие, как

stdin

,

stdout

и

stderr

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

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

<stdio.h>

, если вам нужно сделать что-либо, не определенное стандартом С ISO. Функция

fileno()

возвращает лежащий в основе дескриптор файла:

#include <stdio.h> /* POSIX */

int fileno(FILE *stream);

4.4.2.2. Закрытие всех открытых файлов

Открытые файлы наследуются порожденными процессами от своих родительских процессов. Фактически они являются

общими

. В частности, общим является положение в файле. Подробности мы оставим для дальнейшего обсуждения в разделе 9.1.1.2 «Разделение дескрипторов файлов».

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

int i;

/* оставить лишь 0, 1, и 2 */

for (i = 3; i < getdtablesize(); i++)

4.4.3. Чтение и запись

Ввод/вывод осуществляется системными вызовами

read()

и

write()

соответственно:

#include <sys/types.h> /* POSIX */

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

4.4.4. Пример: Unix

cat

Как и было обещано, вот версия cat V7

[47]

. Она начинается с проверки опций,

cat

V7 принимает единственную опцию,

-u

, для осуществления небуферированного вывода.

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

<stdio.h>

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

1  /*

2   * Объединение файлов.

3   */

Глава 5

Каталоги и служебные данные файлов

Данная глава продолжает подъем по кривой обучения до следующего плато: понимания каталогов и информации о файлах.

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

5.1. Просмотр содержимого каталога

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

5.1.1. Определения

Рис. Copyright 1997-2004 © J.D. «Illiad» Frazer. Использовано по разрешению, http://www.userfriendly.org

Мы начнем обсуждение с определения нескольких терминов.

Раздел (partition)

Единица физического хранилища.

Физические разделы

обычно являются либо частями диска, либо целым диском. Современные системы дают возможность создавать из нескольких физических

логические разделы

.

5.1.2. Содержимое каталога

Каталоги устанавливают связь между именем файла и индексом. Элементы каталога содержат номер индекса и имя файла. Они содержат также дополнительную учетную информацию, которая нам здесь не интересна. См. рис. 5.2.

Рис. 5.2

. Концептуальное содержание каталога

На ранних Unix-системах были двухбайтные номера индексов, а имена файлов — до 14 байтов. Вот полное содержание файла V7

/usr/include/sys/dir.h

:

#ifndef DIRSIZ

5.1.3. Прямые ссылки

Когда файл создается с помощью

open()

или

creat()

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

-i

команды

ls

отображает номер индекса.

$ echo hello, world > message /* Создать новый файл */

$ ls -il message /* Показать также номер индекса */

228786 -rw-r--r-- 1 arnold devel 13 May 4 15:43 message

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

ссылкой (link)

или

прямой ссылкой (hard link)

на файл. Ссылки создаются с помощью команды

ln

. Она используется следующим образом: '

ln старый_файл новый_файл

'.

5.1.3.1. Программа GNU link

Программа

ln

сложная и большая. Однако, GNU Coreutils содержит несложную программу

link

, которая просто вызывает

link()

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

link.с

, не относящиеся к делу части удалены. Номера строк относятся к действительному файлу.

20  /* Обзор реализации:

21

22     Просто вызывает системную функцию 'link' */

23

5.1.3.2. Точка и точка-точка

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

.

' и '

..

'. На самом деле они просто являются прямыми ссылками. В первом случае '

.

' является прямой ссылкой на каталог, содержащий ее, а '

..

' — прямой ссылкой на родительский каталог. Операционная система создает для вас эти ссылки; как упоминалось ранее, код уровня пользователя не может создать прямую ссылку на каталог. Этот пример иллюстрирует ссылки:

$ pwd /* Отобразить текущий каталог */

/tmp

$ ls -ldi /tmp /* Показать номер его индекса */

225345 drwxrwxrwt 14 root root 4096 May 4 16:15 /tmp

5.1.4. Переименование файлов

При данном способе отображения элементами каталога имен на номера индексов, переименование файла концептуально очень просто:

1. Если новое имя файла обозначает существующий файл, сначала удалить этот файл.

2. Создать новую ссылку на файл через новое имя.

3. Удалить старое имя (ссылку) для файла. (Удаление имен обсуждается в следующем разделе.)

Ранние версии команды mv работали таким способом. Однако, при таком способе переименование файла не является

атомарным

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

[51]

, разрушая операцию переименования и подменяя оригинальный файл другим.

5.2. Создание и удаление каталогов

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

mkdir()

и

rmdir()

соответственно:

#include <sys/types.h> /* POSIX */

#include <sys/stat.h>

int mkdir(const char *pathname, mode_t mode);

5.3. Чтение каталогов

В оригинальных системах Unix чтение содержимого каталогов было просто. Программа открывала каталог с помощью

open()

и непосредственно читала двоичные структуры

struct direct

, по 16 байтов за раз. Следующий фрагмент кода из программы V7

rmdir

[53]

, строки 60–74. Он показывает проверку на пустоту каталога.

60 if ((fd = open(name, 0)) < 0) {

61  fprintf(stderr, "rmdir: %s unreadable\n", name);

62  ++Errors;

63  return;

5.3.1. Базовое чтение каталогов

Элементы каталогов представлены

struct dirent

(

не

то же самое, что V7

struct direct

!):

struct dirent {

 ...

 ino_t d_ino;      /* расширение XSI --- см. текст */

 char d_name[...]; /* О размере этого массива см. в тексте */

5.3.1.1. Анализ переносимости

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

readdir()

, всегда будут '

.

' и '

..

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

.

' и '

..

' могут быть в середине каталога или даже вовсе не присутствовать

[55]

.

Во-вторых, стандарт POSIX ничего не говорит о возможных значениях

d_info

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

readdir()

не возвращает пустые элементы, поэтому реализация GNU/Linux

readdir()

не беспокоится с возвратом элементов, когда '

d_ino == 0

'; она переходит к следующему действительному элементу.

Поэтому по крайней мере на системах GNU/Linux и Unix маловероятно, что

d_ino

когда-нибудь будет равен нулю. Однако, лучше по возможности вообще избегать использования этого поля.

Наконец, некоторые системы используют

d_fileno

вместо

d_ino

в

struct dirent

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

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[...];     /* Как ранее */

5.3.2. Функции размещения каталогов BSD

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

#include <dirent.h> /* XSI */

/* Предупреждение: POSIX XSI использует для обеих функций long, а не off_t */

off_t telldir(DIR *dir);              /* Вернуть текущее положение */

5.4. Получение информации о файлах

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

5.4.1. Типы файлов Linux

Linux (и Unix) поддерживает следующие различные типы файлов:

Обычные файлы

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

ls -l

' они обозначаются в виде первого символа '

-

' поля прав доступа (режима).

Каталоги

Специальные файлы для связывания имен файлов с индексами. В листинге '

ls -l

' они обозначаются первым символом

d

поля прав доступа.

5.4.2. Получение информации о файле

Три системных вызова возвращают информацию о файлах:

#include <sys/types.h> /* POSIX */

#include <sys/stat.h>

#include <unistd.h>

5.4.3. Только Linux: указание файлового времени повышенной точности

Ядра Linux 2.6 и более поздние предоставляют в

struct stat

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

st_atime_nsec 

Наносекундная компонента времени доступа к файлу.

st_mtime_nsec 

Наносекундная компонента времени изменения файла

st_ctime_nsec 

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

Некоторые другие системы также предоставляют такие поля с повышенной точностью времени, но имена соответствующих членов структуры

struct stat

не

стандартизованы, что затрудняет написание переносимого кода, использующего эти времена. (Связанные с этим расширенные системные вызовы см. в разделе 14.3.2 «Файловое время в микросекундах:

utimes()

».)

5.4.4. Определение типа файла

Вспомните, что в поле

st_mode

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

<sys/stat.h>

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

true

или

false

при использовании с полем

st_mode

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

struct stat stbuf;

char filename[PATH_МАХ]; /* PATH_MAX из <limits.h> */

/* ... поместить имя файла в filename ... */

if (stat(filename, &stbuf) < 0) {

5.4.4.1. Сведения об устройстве

Стандарт POSIX не определяет значение типа

dev_t

, поскольку предполагалось его использование на не-Unix системах также, как на Unix-системах. Однако стоит знать, что находится в

dev_t

.

Когда истинно

S_ISBLK(sbuf.st_mode)

или

S_ISCHR(sbuf.st_mode)

, сведения об устройстве находятся в поле

sbuf.st_rdev

. В противном случае это поле не содержит никакой полезной информации.

Традиционно файлы устройств Unix кодируют

старший

и

младший

номера устройства в значении

dev_t

. По старшему номеру различают тип устройства, такой, как «дисковый привод» или «ленточный привод». Старшие номера различают также разные типы устройств, такие, как диск SCSI в противоположность диску IDE. Младшие номера различают устройства данного типа, например, первый диск или второй. Вы можете увидеть эти значения с помощью '

ls -l

':

$ ls -l /dev/hda /dev/hda? /* Показать номера для первого жесткого диска */

brw-rw---- 1 root disk 3, 0 Aug 31 2002 /dev/hda

5.4.4.2. Возвращаясь к V7

cat

В разделе 4.4.4 «Пример: Unix cat» мы обещали вернуться к программе V7

cat

, чтобы посмотреть, как она использует системный вызов

stat()

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

31 fstat(fileno(stdout), &statb);

32 statb.st_mode &= S_IFMT;

33 if (statb.st_mode != S_IFCHR && statb.st_mode != S_IFBLK) {

34  dev = statb.st_dev;