КулЛиб электронная библиотека 

Linux API. Исчерпывающее руководство [Майкл Керриск] (fb2) читать онлайн


Настройки текста:



Майкл Керриск Linux API. Исчерпывающее руководство

От автора

Приветствую вас, читателей русскоязычного издания моей книги The Linux Programming Interface.

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

Англоязычное издание этой книги вышло в конце 2010 года. С того времени было выпущено несколько обновлений ядра Linux (их было примерно по пять за год). Несмотря на это, содержимое оригинала книги, а следовательно, и данного перевода, не утратило актуальности и сохранит ее еще на долгие годы. Тому есть две причины.

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

Во-вторых, изменения вносятся в виде дополнений к интерфейсам, рассматриваемым в книге, а не модификаций уже существующих функциональных свойств, описанных в ней же. (Хочу еще раз отметить, что это вполне естественный ход разработки ядра Linux: специалисты прилагают большие усилия к тому, чтобы ничего не нарушить в уже существующем API пользовательского пространства.) Со дня выхода оригинала книги в данный API были внесены изменения. Их перечень (на английском) дотошные читатели могут увидеть на моем сайте по адресу http://man7.org/tlpi/api_changes/.

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

Майкл Керриск (Michael Kerrisk)

Предисловие

Цель книги

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

• файловый ввод/вывод;

• создание и удаление файлов и каталогов;

• создание новых процессов;

• запуск программ;

• установку таймеров;

• взаимодействие между процессами и потоками на одном компьютере;

• взаимодействие между процессами, запущенными на разных компьютерах, объединенных посредством сети.

Такой набор низкоуровневых интерфейсов иногда называют интерфейсом системного программирования.

Несмотря на то что основное внимание уделяется операционной системе Linux, подробно рассмотрены также стандарты и вопросы, связанные с портируемостью. Я четко разграничиваю темы, специфичные для Linux, и функциональные возможности, типичные для большинства реализаций UNIX и описанные в стандарте POSIX, а также в спецификации Single UNIX Specification. Таким образом, эта книга предлагает всеобъемлющее рассмотрение программного интерфейса UNIX/POSIX и ее могут использовать программисты, которые создают приложения, предназначенные для других UNIX-систем, или портируемые программы.


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

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

• программисты и разработчики программного обеспечения, создающие приложения для Linux, других UNIX-систем или иных систем, совместимых со стандартом POSIX;

• программисты, выполняющие портирование приложений из Linux в другие реализации UNIX или из Linux в другие операционные системы (ОС);

• преподаватели и заинтересованные студенты, которые преподают или изучают программирование для Linux или для других UNIX-систем;

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

Предполагается, что у вас есть какой-либо опыт программирования, при этом опыт системного программирования необязателен. Я также рассчитываю на то, что вы разбираетесь в языке программирования C и знаете, как работать в оболочке и пользоваться основными командами Linux или UNIX. Если вы впервые сталкиваетесь с Linux или UNIX, вам будет полезно прочесть главу 2 — в ней приводится обзор основных понятий Linux и UNIX, ориентированный на программиста.

Стандартным справочным руководством по языку C является книга [Kernighan & Ritchie, 1988]. В книге [Harbison & Steele, 2002] этот язык рассмотрен более подробно, а также приведены изменения, появившиеся в стандарте C99. Издание [van der Linden, 1994] дает альтернативное рассмотрение языка С, очень увлекательное и толковое. В книге [Peek et al., 2001] содержится хорошее краткое введение в работу с системой UNIX.


Linux и UNIX

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

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

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

• интерфейс inotify, позволяющий отслеживать изменения файлов и каталогов;

• мандаты (возможности) (capabilities) — позволяют предоставить какому-либо процессу часть прав суперпользователя;

• расширенные атрибуты;

• флаги индексного дескриптора;

• системный вызов clone();

• файловая система /proc;

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


Структура книги

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

• В качестве вводного руководства в программный интерфейс Linux/UNIX. Можно читать книгу от начала до конца. Главы из второй половины издания основаны на материале, изложенном в главах первой половины; ссылки на более поздний материал по возможности сведены к минимуму.

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

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

1. Предварительные сведения и понятия. История UNIX, языка C и Linux, а также обзор стандартов UNIX (глава 1); ориентированное на программиста описание тем, относящихся к Linux и UNIX (глава 2); фундаментальные понятия системного программирования в Linux и UNIX (глава 3).

2. Фундаментальные функции интерфейса системного программирования. Файловый ввод/вывод (главы 4 и 5); процессы (глава 6); выделение памяти (глава 7); пользователи и группы (глава 8); идентификаторы процесса (глава 9); время (глава 10); системные ограничения и возможности (глава 11); получение информации о системе и процессе (глава 12).

3. Более сложные функции интерфейса системного программирования. Буферизация файлового ввода-вывода (глава 13); файловые системы (глава 14); атрибуты файла (глава 15); расширенные атрибуты (глава 16); списки контроля доступа (глава 17); каталоги и ссылки (глава 18); отслеживание файловых событий (глава 19); сигналы (главы 20–22); таймеры (глава 23).

4. Процессы, программы и потоки. Создание процесса, прекращение процесса, отслеживание дочерних процессов и выполнение программ (главы 24–28); потоки POSIX (главы 29–33).

5. Более сложные темы, относящиеся к процессам и программам. Группы процессов, сессии и управление задачами (глава 34); приоритет процессов и диспетчеризация (глава 35); ресурсы процессов (глава 36); демоны (глава 37); написание программ, привилегированных в плане безопасности (глава 38); возможности (глава 39); учетные записи (глава 40); совместно используемые библиотеки (главы 41 и 42).

6. Межпроцессное взаимодействие (IPC). Обзор IPC (глава 43); каналы и очереди FIFO (глава 44); отображение в память (глава 45); операции с виртуальной памятью (глава 46); IPC в стандарте POSIX: очереди сообщений, семафоры и совместно используемая память (главы 47–50); блокировка файлов (глава 51).

7. Сокеты и сетевое программирование. IPC и сетевое программирование с помощью сокетов (главы 52–57).

8. Углубленное рассмотрение вопросов ввода-вывода. Терминалы (глава 58); альтернативные модели ввода-вывода (глава 59); псевдотерминалы (глава 60).


Примеры программ

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

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

Весь исходный код доступен для загрузки с сайта англоязычного издания этой книги: http://www.man7.org/tlpi.

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

Предоставляемый исходный код является свободно распространяемым, и его можно изменять при условии соблюдения лицензии GNU Affero General Public License (Affero GPL) version 3 («Общедоступная лицензия GNU Affero, 3-я версия»), текст которой присутствует в архиве с исходным кодом.

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


Упражнения

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


Стандарты и портируемость

В этой книге особое внимание я уделил вопросам портируемости. Вы обнаружите немало ссылок на соответствующие стандарты, в особенности на объединенный стандарт POSIX.1-2001 и Single UNIX Specification version 3 (SUSv3). Кроме того, я привожу подробные сведения об изменениях в недавней версии — объединенном стандарте POSIX.1-2008 и SUSv4. (Поскольку стандарт SUSv3 гораздо обширнее и является стандартом UNIX с наибольшим влиянием (на момент написания книги), в этом руководстве при рассмотрении стандартов я, как правило, опираюсь на SUSv3, добавляя примечания об отличиях в SUSv4. Тем не менее можно рассчитывать на то, что, за исключением указанных случаев, утверждения о спецификациях в стандарте SUSv3 действуют и в SUSv4.)

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

Я протестировал работу большинства программ, приведенных в этой книге (за исключением тех, что используют функции, отмеченные как специфичные для Linux), в некоторых или во всех этих системах: Solaris, FreeBSD, Mac OS X, Tru64 UNIX и HP-UX. Для улучшения портируемости программ в некоторые из этих систем на сайте http://www.man7.org/tlpi приводятся альтернативные версии большинства примеров с дополнительным кодом, которого нет в этой книге.


Ядро Linux и версии библиотеки C

Основной акцент этой книги сделан на версии Linux 2.6.x, ядро которой было наиболее популярно на момент написания книги. Подробности версии 2.4 также описаны, причем отмечено, чем различаются функции в версиях Linux 2.4 и 2.6. В тех случаях, когда новые функции введены в серии релизов Linux 2.6.x, указана точная версия ядра, в которой они появились (например, 2.6.34).

Что касается библиотеки C, основное внимание уделено GNU-библиотеке C (glibc) 2-й версии. Там, где это важно, приведены различия между версиями glibc 2.x.

Когда это издание готовилось к печати, было выпущено ядро Linux версии 2.6.35, а версия glibc 2.12 уже появилась. Книга написана применительно к этим версиям программного обеспечения. Изменения, которые появились в интерфейсах Linux и в библиотеке glibc после публикации этой книги, будут отмечены на сайте.


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

Хотя примеры программ написаны на языке C, вы можете применять рассмотренные в этой книге интерфейсы из других языков программирования, в частности компилируемых языков C++, Pascal, Modula, Ada, FORTRAN, D, а также таких языков сценариев, как Perl, Python и Ruby. (Для Java необходим другой подход; см., например, работу [Rochkind, 2004].) Понадобятся иные приемы для того, чтобы добиться необходимых определений констант и объявлений функций (за исключением языка C++). Может также потребоваться дополнительная работа для передачи аргументов функции в том стиле, которого требуют соглашения о связях в языке С. Но, несмотря на эти различия, основные понятия не меняются, и вы обнаружите, что информация из этого руководства применима даже при работе с другим языком программирования.

Об авторе

Я начал работать в UNIX и на языке С в 1987 году, когда провел несколько недель за рабочей станцией HP Bobcat, имея при себе первое издание книги Марка Рохкинда «Эффективное UNIX-программирование» (Marc Rochkind, Advanced UNIX Programming) и распечатку руководства по командной оболочке С shell shell (она же csh) (в конечном итоге у нее был довольно потрепанный вид). Подход, который я применял тогда, я стараюсь применять и теперь. Рекомендую его также всем, кто приступает к работе с новой технологией в разработке ПО: потратьте время на чтение документации (если она есть) и пишите небольшие (но постепенно увеличивающиеся) тестовые программы до тех пор, пока не начнете уверенно понимать программное обеспечение. Я обнаружил, что в конечном итоге такой вариант самообучения хорошо окупает себя в плане сэкономленного времени. Многие примеры программ в книге сконструированы так, чтобы подтолкнуть вас к применению этого подхода.

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

Я проработал в Linux почти в два раза меньше, чем в UNIX, но за это время мои интересы еще больше сфокусировались на границе между ядром и пространством пользователя — на программном интерфейсе Linux. Это вовлекло меня в несколько взаимосвязанных видов деятельности. Время от времени я предоставлял исходную информацию и отчеты об ошибках для стандарта POSIX/SUS, выполнял тестирование и экспертную оценку новых интерфейсов пространства пользователя, добавленных к ядру Linux (и помог обнаружить и исправить множество ошибок в коде и дизайне этих интерфейсов). Я также регулярно выступал на конференциях, посвященных интерфейсам и связанной с ними документации. Меня приглашали на несколько ежегодных совещаний Linux Kernel Developers Summit (саммит разработчиков ядра Linux). Общей нитью, которая связывает все эти виды деятельности воедино, является мой наиболее заметный вклад в мир Linux — работа над проектом man-pages (http://www.kernel.org/doc/man-pages/).

Названный проект лежит в основе страниц руководства Linux в разделах 2–5 и 7. Эти страницы описывают программные интерфейсы, которые предоставляются ядром Linux и GNU-библиотекой C, — тот же охват тем, что и в этой книге. Я занимался проектом man-pages более десяти лет. Начиная с 2004 года я сопровождаю его. В эту задачу входят приблизительно в равных долях написание документации, изучение исходного кода ядра и библиотеки, а также создание программ для проверки деталей. (Документирование интерфейса — прекрасный способ обнаружить ошибки в этом интерфейсе.) Кроме того, я самый продуктивный участник проекта man-pages: он содержит около 900 страниц, 140 из них написал я один и 125 — в соавторстве. Поэтому весьма вероятно, что вы уже читали что-либо из моих публикаций еще до того, как приобрели эту книгу. Надеюсь, что информация вам пригодилась, и также надеюсь, что эта книга окажется еще более полезной.

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

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

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

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

• Кристоф Блэсс (Christophe Blaess) является программистом-консультантом и профессиональным преподавателем. Специализируется на производственных (в реальном времени и встраиваемых) приложениях на основе Linux. Он автор замечательной книги на французском языке Programmation systиme en C sous Linux («Системное программирование на языке С в Linux»), в которой охвачены многие из тем, изложенных в данной книге. Он прочитал и щедро прокомментировал многие главы моей книги.

• Дэвид Бутенхоф (David Butenhof) из компании Hewlett-Packard — участник самой первой рабочей группы по потокам POSIX, а также по расширениям стандарта Single UNIX Specification для потоков. Он автор книги Programming with POSIX Threads («Программирование с помощью потоков POSIX»). Он написал исходную базовую реализацию потоков DCE Threads для проекта Open Software Foundation и был ведущим проектировщиком реализации потоков для проектов OpenVMS и Digital UNIX. Дэвид проверил главы о потоках, предложил множество улучшений и терпеливо помогал мне лучше разобраться в некоторых особенностях API для потоков POSIX.

• Джеф Клэр (Geoff Clare) занят в проекте The Open Group разработкой комплексов тестирования на соответствие стандартам UNIX. Он уже более 20 лет принимает участие в разработке стандартов UNIX и является одним из немногих ключевых участников группы Austin Group, которая создает объединенный стандарт, образующий спецификацию POSIX.1 и основные части спецификации Single UNIX Specification. Джеф тщательно проверил части рукописи, относящиеся к стандартным интерфейсам UNIX, терпеливо и вежливо предлагая свои исправления и улучшения. Он выявил малозаметные ошибки в коде и помог сосредоточиться на важности следования стандартам при создании портируемых программ.

• Лоик Домэнье (Loic Domaigne), работавший в немецкой авиадиспетчерской службе, — разработчик программных комплексов, который проектирует и создает распределенные параллельные отказоустойчивые встроенные системы с жесткими требованиями работы в реальном времени. Он предоставил обзорный вводный материал для спецификации потоков в стандарте SUSv3. Лоик — замечательный преподаватель и эрудированный участник различных технических онлайн-форумов. Он тщательно проверил главы о потоках, а также многие другие разделы книги. Он также написал несколько хитроумных программ для проверки особенностей реализации потоков в Linux, а также предложил множество идей по улучшению общей подачи материала.

• Герт Деринг (Gert Doring) написал программы mgetty и sendfax — наиболее популярные свободно распространяемые пакеты для работы с факсами в UNIX и Linux. В настоящее время он главным образом работает над созданием обширных сетей на основе протоколов IPv4 и IPv6 и управлением ими. Эта деятельность включает в себя сотрудничество с коллегами по всей Европе с целью определения рабочих политик, которые обеспечивают надежную работу инфраструктуры Интернета. Герт дал немало ценных советов по главам, описывающим терминалы, учетные записи, группы процессов, сессии и управление задачами.

• Вольфрам Глоджер (Wolfram Gloger) — ИТ-консультант, который последние 15 лет нередко участвовал в различных проектах FOSS (Free and Open Source Software, свободно распространяемое ПО и ПО с открытым исходным кодом). Среди прочего, Вольфрам является разработчиком пакета malloc, который используется в GNU-библиотеке C. В настоящее время он разрабатывает веб-сервисы для дистанционного обучения, но иногда все так же занимается ядром и системными библиотеками. Вольфрам проверил несколько глав и особенно помог мне при рассмотрении вопросов, относящихся к памяти.

• Фернандо Гонт (Fernando Gont) — сотрудник центра CEDI (Centro de Estudios de Informatica) при аргентинском университете Universidad Tecnologica Nacional. Он занимается в основном интернет-разработками и активно участвует в работе сообщества IETF (Internet Engineering Task Force, Инженерный совет Интернета), для которого он написал несколько рабочих предложений. Фернандо также занят оценкой безопасности коммуникационных протоколов в британском центре CPNI (Centre for the Protection of National Infrastructure, Центр защиты государственной инфраструктуры). Он впервые выполнил всестороннюю оценку безопасности протоколов TCP и IP. Фернандо очень тщательно проверил главы, посвященные сетевому программированию, объяснил множество особенностей протоколов TCP/IP, а также предложил немало улучшений.

• Андреас Грюнбахер (Andreas Grunbacher) — специалист по ядру и автор реализации расширенных атрибутов в Linux, а также списков контроля доступа в стандарте POSIX. Андреас тщательно проверил многие главы, оказал существенную поддержку, а один из его комментариев значительно повлиял на структуру книги.

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

• Андреас Егер (Andreas Jaeger) руководил портированием Linux в архитектуру x86-64. Будучи разработчиком GNU-библиотеки C, он портировал эту библиотеку и сумел добиться ее соответствия стандартам в различных областях, особенно в ее математической части. В настоящее время он является менеджером проектов openSUSE в компании Novell. Андреас проверил намного больше глав, чем я рассчитывал, предложил множество улучшений и воодушевил на дальнейшую работу с книгой.

• Рик Джонс (Rick Jones), который известен также как «Мистер Netperf» (Networked Systems Performance Curmudgeon (буквально — «старый ворчун на тему производительности сетевых систем») в компании Hewlett-Packard), дотошно проверил главы о сетевом программировании.

• Энди Клин (Andi Kleen) (работавший тогда в компании SUSE Labs) — знаменитый профессионал, который работал над различными характеристиками ядра Linux, включая сетевое взаимодействие, обработку ошибок, масштабируемость и программный код низкоуровневой архитектуры. Энди досконально проверил материал о сетевом программировании, расширил мое представление о большинстве особенностей реализации протоколов TCP/IP в Linux и дал немало советов, позволивших улучшить подачу материала.

• Мартин Ландерс (Martin Landers) (компания Google) был еще студентом, когда мне посчастливилось познакомиться с ним в колледже. За короткий срок он успел добиться многого, поработав и проектировщиком архитектуры ПО, и ИТ-преподавателем, и профессиональным программистом. Мне повезло, что Мартин оказался в числе моих экспертов. Его многочисленные язвительные комментарии и исправления значительно улучшили многие главы этой книги.

• Джейми Лоукир (Jamie Lokier) — известный специалист, который в течение 15 лет участвует в разработке Linux. В настоящее время он характеризует себя как «консультант по решению сложных проблем, в которые каким-либо образом вовлечена Linux». Джейми тщательнейшим образом проверил главы об отображении в память, совместно используемой памяти POSIX и об операциях в виртуальной памяти. Благодаря его комментариям я гораздо лучше стал разбираться в этих темах и смог существенно улучшить структуру глав книги.

• Барри Марголин (Barry Margolin) за время своей 25-летней карьеры работал системным программистом, системным администратором и инженером службы поддержки. В настоящее время он является ведущим инженером по производительности в компании Akamai Technologies. Он популярный и уважаемый автор сообщений в онлайн-форумах об UNIX и Интернете, а также рецензент множества книг на эти темы. Барри проверил несколько глав моей книги и предложил много улучшений.

• Павел Плужников (Paul Pluzhnikov) (компания Google) в прошлом был техническим руководителем и ключевым разработчиком инструмента для отладки памяти Insure++. Он отлично разбирается в отладчике gdb и часто отвечает посетителям форумов об отладке, выделении памяти, совместно используемых библиотеках и состоянии переменных среды в момент работы программы. Павел просмотрел многие главы и внес несколько ценных предложений.

• Джон Рейзер (John Reiser) (совместно с Томом Лондоном (Tom London)) осуществил одно из самых первых портирований UNIX в 32-битную архитектуру: вариант VAX-11/780. Он создал системный вызов mmap(). Джон проверил многие главы (включая, разумеется, и главу о системном вызове mmap()) и привел множество исторических подробностей.

• Энтони Робинс (Anthony Robins) (адъюнкт-профессор по информатике в университете города Отаго в Новой Зеландии), мой близкий друг вот уже более трех десятилетий, стал первым читателем черновиков некоторых глав. Он предложил немало ценных замечаний на раннем этапе и оказал поддержку по мере развития проекта.

• Михаэль Шрёдер (Michael Schröder) (компания Novell) — один из главных авторов GNU-программы screen. Работа над ней научила его хорошо разбираться в тонкостях и различиях в реализации драйверов терминалов. Михаэль проверил главы о терминалах и псевдотерминалах, а также главу о группах процессов, сессиях и управлении задачами, дав ценные отзывы.

• Манфред Спрол (Manfred Spraul), разрабатывавший среди прочего код межпроцессного взаимодействия (IPC) в ядре Linux, тщательно проверил некоторые главы о нем и предложил множество улучшений.

• Том Свигг (Tom Swigg), в прошлом преподаватель UNIX в компании Digital, выступил в роли эксперта на ранних стадиях. Том уже более 25 лет работает инженером-программистом и ИТ-преподавателем и в настоящее время трудится в лондонском университете South Bank, где занимается программированием и поддержкой Linux и среды VMware.

• Йенс Томс Тёрринг (Jens Thoms Törring) является представителем поколения физиков, которые превратились в программистов. Он разработал множество драйверов устройств с открытым кодом, а также другое ПО. Йенс прочитал на удивление разнородные главы и поделился исключительно ценными соображениями о том, в чем можно улучшить каждую из них.

Многие другие технические эксперты также прочитали различные части этой книги и предложили ценные комментарии. Благодарю вас: Джордж Анцингер (George Anzinger) (компания MontaVista Software), Стефан Бечер (Stefan Becher), Кшиштоф Бенедичак (Krzysztof Benedyczak), Дэниэл Бранеборг (Daniel Brahneborg), Эндрис Брауэр (Andries Brouwer), Анабел Черч (Annabel Church), Драган Цветкович (Dragan Cvetkovič), Флойд Л. Дэвидсон (Floyd L. Davidson), Стюарт Дэвидсон (Stuart Davidson) (компания Hewlett-Packard Consulting), Каспер Дюпон (Kasper Dupont), Петер Феллингер (Peter Fellinger) (компания jambit GmbH), Мел Горман (Mel Gorman) (компания IBM), Нильс Голлеш (Niels Gцllesch), Клаус Гратцл (Claus Gratzl), Серж Халлин (Serge Hallyn) (компания IBM), Маркус Хартингер (Markus Hartinger) (компания jambit GmbH), Ричард Хендерсон (Richard Henderson) (компания Red Hat), Эндрю Джоузи (Andrew Josey) (компания The Open Group), Дэн Кегел (Dan Kegel) (компания Google), Давид Либенци (Davide Libenzi), Роберт Лав (Robert Love) (компания Google), Х. Дж. Лу (H. J. Lu) (компания Intel Corporation), Пол Маршалл (Paul Marshall), Крис Мэйсон (Chris Mason), Майкл Матц (Michael Matz) (компания SUSE), Тронд Майклбаст (Trond Myklebust), Джеймс Пич (James Peach), Марк Филлипс (Mark Phillips) (компания Automated Test Systems), Ник Пиггин (Nick Piggin) (компании SUSE Labs и Novell), Кай Йоханнес Поттхофф (Kay Johannes Potthoff), Флориан Рампп (Florian Rampp), Стефен Ротвелл (Stephen Rothwell) (компании Linux Technology Centre и IBM), Маркус Швайгер (Markus Schwaiger), Стефен Твиди (Stephen Tweedie) (компания Red Hat), Бритта Варгас (Britta Vargas), Крис Райт (Chris Wright), Михал Вронски (Michal Wronski) и Умберто Замунер (Umberto Zamuner).

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

Спасибо следующим людям за их ответы на технические вопросы: Яну Кара (Jan Kara), Дейву Клайкампу (Dave Kleikamp) и Джону Снейдеру (Jon Snader). Благодарю Клауса Гратцла и Пола Маршалла за помощь в системном менеджменте.

Спасибо компании Linux Foundation (LF), которая на протяжении 2008 года оплачивала мою полную занятость в качестве стипендианта при работе над проектом man-pages, а также при тестировании и экспертной оценке программного интерфейса Linux. И хотя компания не оказывала непосредственную финансовую поддержку работы над этой книгой, она все же финансово поддерживала меня и мою семью, а возможность сконцентрироваться на документировании и тестировании программного интерфейса Linux благоприятно отразилась на моем «частном» проекте. На индивидуальном уровне — спасибо Джиму Землину (Jim Zemlin) за то, что он оказался в роли моего «интерфейса» при работе в LF, а также членам Технического совета компании (LF Technical Advisory Board), которые поддержали мою заявку на принятие в число стипендиантов.

Благодарю Алехандро Фореро Куэрво (Alejandro Forero Cuervo), который предложил название для этой книги!

Более 25 лет назад, когда я получал первую ученую степень, Роберт Биддл (Robert Biddle) заинтриговал меня рассказами об UNIX и языках С и Ratfor. Искренне благодарю его.

Спасибо следующим людям, которые не были непосредственно связаны с этим проектом, но воодушевили меня на получение второй ученой степени в университете Кентербери в Новой Зеландии. Это Майкл Ховард (Michael Howard), Джонатан Мэйн-Уиоки (Jonathan Mane-Wheoki), Кен Стронгман (Ken Strongman), Гарт Флетчер (Garth Fletcher), Джим Поллард (Jim Pollard) и Брайан Хейг (Brian Haig).

Уже довольно давно Ричард Стивенс (Richard Stevens) написал несколько превосходных книг о UNIX-программировании и протоколах TCP/IP. Для меня, а также для многих других программистов, эти издания стали прекрасным источником технической информации на долгие годы.

Спасибо следующим людям и организациям, которые обеспечили меня UNIX-системами, позволили проверить тестовые программы и уточнить детали для других реализаций UNIX: Энтони Робинсу (Anthony Robins) и Кэти Чандра (Cathy Chandra) — за системы тестирования в Университете Отаго в Новой Зеландии; Мартину Ландерсу (Martin Landers), Ральфу Эбнеру (Ralf Ebner) и Клаусу Тилку (Klaus Tilk) — за системы тестирования в Техническом университете Мюнхена в Германии; компании Hewlett-Packard за то, что сделала свободно доступными в Интернете свои системы testdrive; Полю де Веерду (Paul de Weerd) — за доступ к системе OpenBSD.

Искренне признателен двум мюнхенским компаниям и их владельцам, которые не только предоставили мне гибкий график работы и приветливых коллег, но и оказались исключительно великодушны, позволив мне пользоваться их офисами на время написания книги. Спасибо Томасу Кахабке (Thomas Kahabka) и Томасу Гмельху (Thomas Gmelch) из компании exolution GmbH, а отдельное спасибо — Петеру Феллингеру и Маркусу Хартингеру из компании jambit GmbH.

Спасибо за разного рода помощь, полученную от вас, Дэн Рэндоу (Dan Randow), Карен Коррел (Karen Korrel), Клаудио Скалмацци (Claudio Scalmazzi), Майкл Шюпбах (Michael Schüpbach) и Лиз Райт (Liz Wright). Благодарю Роба Суистеда (Rob Suisted) и Линли Кук (Lynley Cook) за фотографии, которые использованы на обложке.

Спасибо следующим людям, которые всевозможными способами подбадривали и поддерживали меня при работе над этим проектом: это Дебора Черч (Deborah Church), Дорис Черч (Doris Church) и Энни Карри (Annie Currie).

Спасибо сотрудникам издательства No Starch Press за все виды содействия этому внушительному проекту. Благодарю Билла Поллока (Bill Pollock) за то, что удалось договориться с ним с самого начала, за его незыблемую уверенность в проекте и за терпеливое сопровождение. Спасибо моему первому выпускающему редактору Меган Дунчак (Megan Dunchak). Спасибо корректору Мэрилин Смит (Marilyn Smith), которая обязательно найдет множество огрехов, несмотря на то что я изо всех сил стремлюсь к ясности и согласованности. Райли Хоффман (Riley Hoffman) всецело отвечал за макет и дизайн этой книги, а также взял на себя роль выпускающего редактора, когда мы вышли на финишную прямую. Райли милостиво вытерпел мои многочисленные запросы, касающиеся подходящего макета, и в итоге выдал превосходный результат. Спасибо!

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


Разрешения

Институт инженеров электротехники и электроники (IEEE) и компания The Open Group любезно предоставили право цитировать фрагменты текста из документов IEEE Std 1003.1, 2004 Edition (Стандарт IEEE 1003.1, версия 2004 года), Standard for Information Technology — Portable Operating System Interface (POSIX) (Стандарт информационных технологий — портируемый интерфейс операционной системы) и The Open Group Base Specifications Issue 6 (Базовые спецификации Open Group. Выпуск 6). Полную версию стандарта можно прочитать на сайте http://www.unix.org/version3/online.html.


Обратная связь

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

Майкл Тимоти Керриск

Мюнхен, Германия — Крайстчерч, Новая Зеландия

Август 2010 г.

mtk@man7.org

1. История и стандарты

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

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

Что касается самого понятия UNIX, то в мире бытуют два определения. Одно из них указывает на те операционные системы, которые прошли официальную проверку на совместимость с единой спецификацией под названием Single UNIX Specification и в результате этого получили от владельца торговой марки UNIX, The Open Group, официальное право называться UNIX. На момент написания книги это право не было получено ни одной из свободно распространяемых реализаций UNIX (например, Linux и FreeBSD).

Согласно другому общепринятому значению определение UNIX распространяется на те системы, которые по внешнему виду и поведению похожи на классические UNIX-системы (например, на исходную версию Bell Laboratories UNIX и ее более поздние ветки — System V и BSD). В соответствии с этим определением Linux, как правило, считается UNIX-системой (как и современные BSD-системы). Хотя в этой книге спецификации Single UNIX Specification уделяется самое пристальное внимание, мы последуем второму определению UNIX и поэтому позволим себе довольно частое использование фраз вроде «Linux, как и другие реализации UNIX…».


1.1. Краткая история UNIX и языка C

Первая реализация UNIX была разработана в 1969 году (в год рождения Линуса Торвальдса (Linus Torvalds)) Кеном Томпсоном (Ken Thompson) в компании Bell Laboratories, являвшейся подразделением телефонной корпорации AT&T. Эта реализация была написана на ассемблере для мини-компьютера Digital PDP-7. Название UNIX было выбрано из-за созвучия с MULTICS (Multiplexed Information and Computing Service), названием более раннего проекта операционной системы (ОС), разрабатываемой AT&T в сотрудничестве с институтом Massachusetts Institute of Technology (MIT) и компанией General Electric. (К тому времени AT&T уже была выведена из проекта из-за срыва первоначальных планов по разработке экономически пригодной системы.) Томпсон позаимствовал у MULTICS ряд идей для своей новой операционной системы, включая древовидную структуру файловой системы, отдельную программу для интерпретации команд (оболочки) и понятие файлов как неструктурированных потоков байтов.

В 1970 году UNIX была переписана на языке ассемблера для только что приобретенного мини-компьютера Digital PDP-11, который в то время считался новой и довольно мощной машиной. Следы PDP-11 до сих пор могут обнаруживаться в большинстве реализаций UNIX, включая Linux, под различными названиями.

Некоторое время спустя один из коллег Томпсона по Bell Laboratories, с которым он на ранней стадии сотрудничал при создании UNIX, Деннис Ритчи (Dennis Ritchie), разработал и реализовал язык программирования C. Процесс создания носил эволюционный характер; C был последователем более раннего языка программирования В, код которого выполнялся в режиме интерпретации. Язык B был изначально реализован Томпсоном и впитал в себя множество его идей, позаимствованных из еще более раннего языка программирования под названием BCPL. К 1973 году язык C уже был доведен до состояния, позволившего почти полностью переписать на нем ядро UNIX. Таким образом, UNIX стала одной из самых ранних ОС, написанных на языке высокого уровня, что позволило в дальнейшем портировать ее на другие аппаратные архитектуры.

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


UNIX от первого до шестого выпуска

В период с 1969 по 1979 год вышло несколько выпусков UNIX, называемых редакциями. По сути, они были текущими вариантами развивающейся версии, которая разрабатывалась в компании AT&T. В издании [Salus, 1994] указываются следующие даты первых шести редакций UNIX.

• Первая редакция, ноябрь 1971 года. К этому времени UNIX работала на PDP-11 и уже имела компилятор FORTRAN и версии множества программ, используемых по сей день, включая ar, cat, chmod, chown, cp, dc, ed, find, ln, ls, mail, mkdir, mv, rm, sh, su и who.

• Вторая редакция, июнь 1972 года. К этому моменту UNIX была установлена на десяти машинах компании AT&T.

• Третья редакция, февраль 1973 года. В эту редакцию был включен компилятор языка C и первая реализация конвейеров (pipes).

• Четвертая редакция, ноябрь 1973 года. Это была первая версия, практически полностью написанная на языке C.

• Пятая редакция, июнь 1974 года. К этому времени UNIX была установлена более чем на 50 системах.

• Шестая редакция, май 1975 года. Это была первая редакция, широко использовавшаяся вне компании AT&T.

За время выхода этих редакций система UNIX стала активнее использоваться, а ее репутация — расти, сначала в рамках компании AT&T, а затем и за ее пределами. Важным вкладом в эту популярность была публикация статьи о UNIX в журнале Communications of the ACM [Ritchie & Thompson, 1974].

К этому времени компания AT&T владела санкционированной правительством монополией на телефонные системы США. Условия соглашения AT&T с правительством США не позволяли компании заниматься продажей программного обеспечения, а это означало, что она не могла продавать UNIX. Вместо этого начиная с 1974 года, с выпуском пятой и особенно с выпуском шестой редакции, AT&T за символическую плату организовала лицензированное распространение UNIX для использования в университетах. Распространяемые для университетов пакеты включали документацию и исходный код ядра (на то время около 10 000 строк кода).

Эта кампания стала существенным вкладом в популяризацию использования операционной системы, и к 1977 году UNIX работала примерно в 500 местах, включая 125 университетов в США и некоторых других странах. UNIX была для университетов весьма дешевой, но при этом мощной интерактивной многопользовательской операционной системой, в то время как коммерческие операционные системы стоили очень дорого. Кроме того, факультеты информатики получали исходный код реальной операционной системы, который они могли изменять и предоставлять своим студентам для изучения и проведения экспериментов. Одни студенты, вооружившись знаниями операционной системы UNIX, превратились в ее ярых приверженцев. Другие пошли еще дальше, основав новые компании или присоединившись к таким компаниям для продажи недорогих компьютерных рабочих станций с запускаемой на них легко портируемой операционной системой UNIX.


Рождение BSD и System V

В январе 1979 года вышла седьмая редакция UNIX. Она повысила надежность системы и предоставила усовершенствованную файловую систему. Этот выпуск также содержал несколько новых инструментальных средств, включая awk, make, sed, tar, uucp, Bourne shell и компилятор языка FORTRAN 77. Значимость седьмой редакции обуславливалась также тем, что, начиная с этого выпуска, UNIX разделилась на два основных варианта: BSD и System V, истоки которых мы сейчас кратко рассмотрим.

Кен Томпсон (Ken Thompson) в 1975/1976 учебном году был приглашенным профессором Калифорнийского университета в Беркли, откуда он в свое время выпустился. Там он работал с несколькими студентами выпускного курса, добавляя к UNIX множество новых свойств. (Один из этих студентов, Билл Джой (Bill Joy), впоследствии стал сооснователем компании Sun Microsystems, которая вскоре заявила о себе на рынке рабочих станций UNIX.) Со временем в Беркли было разработано множество новых инструментов и функций, включая C shell, редактор vi. Кроме того, были усовершенствованы файловая система (Berkeley Fast File System), почтовый агент sendmail, компилятор языка Pascal и система управления виртуальной памятью на новой архитектуре Digital VAX.

Эта версия UNIX, включавшая свой собственный исходный код, получила весьма широкое распространение под названием Berkeley Software Distribution (BSD). Первым полноценным дистрибутивом, появившимся в декабре 1979 года, стал 3BSD. (Ранее выпущенные в Беркли дистрибутивы BSD и 2BSD представляли собой не полные дистрибутивы UNIX, а пакеты новых инструментов, разработанных в Беркли.)

В 1983 году группа исследования компьютерных систем — Computer Systems Research Group — из Калифорнийского университета в Беркли выпустила 4.2BSD. Этот выпуск был примечателен тем, что в нем содержалась полноценная реализация протокола TCP/IP, включая интерфейс прикладного программирования (API) сокетов, и множество различных средств для работы в сети. Выпуск 4.2BSD и его предшественник 4.1BSD стали активно распространяться в университетах по всему миру. Они также легли в основу SunOS (впервые выпущенную в 1983 году) — UNIX-вариант, продаваемый компанией Sun. Другими примечательными выпусками BSD были 4.3BSD в 1986 году и последний выпуск — 4.4BSD — в 1993 году.

Самое первое портирование (перенос) системы UNIX на оборудование, отличное от PDP-11, произошло в 1977–1978 годах, когда Деннис Ритчи и Стив Джонсон (Steve Johnson) портировали ее на Interdata 8/32, а Ричард Миллер (Richard Miller) из Воллонгонского университета в Австралии одновременно с ними портировал ее на Interdata 7/32. Портированная версия Berkeley Digital VAX базировалась на более ранней (1978 года), также портированной версии, созданной Джоном Рейзером (John Reiser) и Томом Лондоном (Tom London). Она называлась 32V и была по сути тем же самым, что и седьмая редакция для PDP-11, за исключением более обширного адресного пространства и более емких типов данных.

В то же время принятое в США антимонопольное законодательство привело к разделу компании AT&T (юридический процесс начался в середине 1970-х годов, а сам раздел произошел в 1982 году), за которым последовали утрата монополии на телефонные системы и приобретение компанией права вывода UNIX на рынок. В результате в 1981 году состоялся выпуск System III (три). Эта версия была создана организованной в компании AT&T группой поддержки UNIX (UNIX Support Group, USG). В ней работали сотни специалистов, занимавшихся усовершенствованием UNIX и созданием приложений для этой системы (в частности, созданием пакетов подготовки документов и средств разработки ПО). В 1983 году последовал первый выпуск System V (пять). Несколько последующих выпусков привели к тому, что в 1989 году состоялся окончательный выпуск System V Release 4 (SVR4), ко времени которого в System V было перенесено множество свойств из BSD, включая сетевые объекты. Лицензия на System V была выдана множеству коммерческих поставщиков, использовавших эту версию как основу своих собственных реализаций UNIX.

Таким образом, вдобавок к различным дистрибутивам BSD, распространявшимся через университеты в конце 1980-х годов, UNIX стала доступна в виде коммерческих реализаций на различном оборудовании. Они включали:

• разработанную в компании Sun операционную систему SunOS, а позже и Solaris;

• созданные в компании Digital системы Ultrix и OSF/1 (в настоящее время, после нескольких переименований и поглощений, HP Tru64 UNIX);

• AIX компании IBM;

• HP-UX компании Hewlett-Packard (HP);

• NeXTStep компании NeXT;

• A/UX для Apple Macintosh;

• XENIX для архитектуры Intel x86-32 компаний Microsoft и SCO. (В данной книге реализация Linux для x86-32 будет упоминаться как Linux/x86-32.)

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

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


1.2. Краткая история Linux

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


1.2.1. Проект GNU

В 1984 году весьма талантливый программист Ричард Столлман (Richard Stallman), работавший в Массачусетском технологическом институте, приступил к созданию «свободно распространяющейся» реализации UNIX. Работа была затеяна Столлманом из этических соображений, и принцип свободного распространения был определен в юридическом, а не в финансовом смысле (см. статью по адресу http://www.gnu.org/philosophy/free-sw.html). Но, как бы то ни было, под сформулированной Столлманом правовой свободой подразумевалось, что такие программные средства, как операционные системы, должны быть доступны на бесплатной основе или поставляться по весьма скромной цене.

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

В ответ на это Столлман запустил проект GNU (рекурсивно определяемый акроним, взятый из фразы GNU’s not UNIX). Он хотел разработать полноценную, находящуюся в свободном доступе UNIX-подобную систему, состоящую из ядра и всех сопутствующих программных пакетов, и призвал присоединиться к нему всех остальных программистов. В 1985 году Столлман основал Фонд свободного программного обеспечения — Free Software Foundation (FSF), некоммерческую организацию для поддержки проекта GNU, а также для разработки совершенно свободного ПО.

Когда был запущен проект GNU, в понятиях, введенных Столлманом, версия BSD не была свободной. Для использования BSD по-прежнему требовалось получить лицензию от AT&T, и пользователи не могли свободно изменять и распространять дальше код AT&T, формирующий часть BSD.

Одним из важных результатов появления проекта GNU была разработка общедоступной лицензии — GNU General Public License (GPL). Она стала правовым воплощением представления Столлмана о свободном программном обеспечении. Большинство программных средств в дистрибутиве Linux, включая ядро, распространяются под лицензией GPL или одной из нескольких подобных лицензий. Программное обеспечение, распространяемое под лицензией GPL, должно быть доступно в форме исходного кода и должно предоставлять право дальнейшего распространения в соответствии с положениями GPL. Внесение изменений в программы, распространяемые под лицензией, не запрещено, но любое распространение такой измененной программы должно также производиться в соответствии с положениями о GPL-лицензировании. Если измененное программное средство распространяется в исполняемом виде, автор также должен дать всем получателям возможность приобрести измененные исходные коды с затратами, не дороже носителя, на котором они находятся. Первая версия GPL была выпущена в 1989 году. Текущая, третья версия этой лицензии, выпущена в 2007 году. До сих пор используется и вторая версия, выпущенная в 1991 году: именно она применяется для ядра Linux. (Различные лицензии свободно распространяемого программного обеспечения рассматриваются в источниках [St. Laurent, 2004] и [Rosen, 2005].)

В рамках проекта GNU так и не было создано работающее ядро UNIX. Но под эгидой этого проекта разработано множество других разнообразных программ. Поскольку эти программы были созданы для работы под управлением UNIX-подобных операционных систем, они могут использоваться и используются на существующих реализациях UNIX и в некоторых случаях даже портируются на другие ОС. Среди наиболее известных программ, созданных в рамках проекта GNU, можно назвать текстовый редактор Emacs, пакет компиляторов GCC (изначально назывался компилятором GNU C, но теперь переименован в пакет GNU-компиляторов, содержащий компиляторы для C, C++ и других языков), оболочка bash и glibc (GNU-библиотека C).

В начале 1990-х годов в рамках проекта GNU была создана практически завершенная система, за исключением одного важного компонента: рабочего ядра UNIX. Проект GNU и Фонд свободного программного обеспечения начали работу над амбициозной конструкцией ядра, известной как GNU Hurd и основанной на микроядре Mach. Но ядро Hurd до сих пор находится не в том состоянии, чтобы его можно было выпустить. (На время написания этой книги работа над Hurd продолжалась и это ядро могло запускаться только на машинах с архитектурой x86-32.)

Значительная часть программного кода, составляющего то, что обычно называют системой Linux, фактически была взята из проекта GNU, поэтому при ссылке на всю систему Столлман предпочитает использовать термин GNU/Linux. Вопрос, связанный с названием (Linux или GNU/Linux) стал причиной дебатов в сообществе разработчиков свободного программного обеспечения. Поскольку данная книга посвящена в основном API ядра Linux, в ней чаще всего будет использоваться термин Linux.

Начало было положено. Чтобы соответствовать полноценной UNIX-системе, созданной в рамках проекта GNU, требовалось только рабочее ядро.


1.2.2. Ядро Linux

В 1991 году Линус Торвальдс (Linus Torvalds), финский студент хельсинкского университета, задумал создать операционную систему для своего персонального компьютера с процессором Intel 80386. Во время учебы он имел дело с Minix, небольшим UNIX-подобным ядром операционной системы, разработанным в середине 1980-х годов Эндрю Таненбаумом (Andrew Tanenbaum), профессором голландского университета. Таненбаум распространял Minix вместе с исходным кодом как средство обучения проектированию ОС в рамках университетских курсов. Ядро Minix могло быть собрано и запущено в системе с процессором Intel 80386. Но, поскольку оно в первую очередь рассматривалось в качестве учебного пособия, ядро было разработано с прицелом на максимальную независимость от архитектуры аппаратной части и не использовало все преимущества, предоставляемые процессорами Intel 80386.

По этой причине Торвальдс приступил к созданию эффективного полнофункционального ядра UNIX для работы на машине с процессором Intel 80386. Через несколько месяцев он спроектировал основное ядро, позволявшее компилировать и запускать различные программы, разработанные в рамках проекта GNU. Затем, 5 октября 1991 года, Торвальдс обратился за помощью к другим программистам, анонсировав версию своего ядра под номером 0.02 в следующем, теперь уже широко известном (многократно процитированном) сообщении в новостной группе Usenet:

«Вы скорбите о тех временах, когда мужчины были настоящими мужчинами и сами писали драйверы устройств? У вас нет хорошего проекта и вы мечтаете вонзить свои зубы в какую-нибудь ОС, чтобы модифицировать ее для своих нужд? Вас раздражает то, что все работает под Minix? И не требуется просиживать ночи, чтобы заставить программу работать? Тогда это послание адресовано вам. Месяц назад я уже упоминал, что работаю над созданием свободной версии Minix-подобной операционной системы для компьютеров семейства AT-386. И вот наконец моя работа достигла той стадии, когда системой уже можно воспользоваться (хотя, может быть, и нет, все зависит от того, что именно вам нужно), и у меня появилось желание обнародовать исходный код для его свободного распространения. Пока это лишь версия 0.02…, но под ее управлением мне уже удалось вполне успешно запустить такие программные средства, как bash, gcc, gnu-make, gnu-sed, compress и так далее».

По сложившейся со временем традиции присваивать клонам UNIX имена, оканчивающиеся на букву X, ядро в конечном итоге получило название Linux. Изначально оно было выпущено под более ограничивающую лицензию, но вскоре Торвальдс сделал его доступным под лицензией GNU GPL.

Призыв к поддержке оказался эффективным. Для разработки Linux к Торвальдсу присоединились другие программисты. Они начали добавлять новую функциональность: усовершенствованную файловую систему, поддержку сетевых технологий, использование драйверов устройств и поддержку многопроцессорных систем. К марту 1994 года разработчики смогли выпустить версию 1.0. В марте 1995 года появилась версия Linux 1.2, в июне 1996 года — Linux 2.0, затем, в январе 1999 года, вышла версия Linux 2.2, а в январе 2001 года была выпущена версия Linux 2.4. Работа над созданием ядра версии 2.5 началась в ноябре 2001 года, что в декабре 2003 года привело к выпуску версии Linux 2.6.


Отступление: версии BSD

Следует заметить, что в начале 1990-х годов уже была доступна еще одна свободная версия UNIX для машин с архитектурой x86-32. Портированную на архитектуру x86-32 версию вполне состоявшейся к тому времени системы BSD под названием 386/BSD разработали Билл (Bill) и Линн Джолиц (Lynne Jolitz). Она была основана на выпуске BSD Net/2 (июнь 1991 года) — версии исходного кода 4.3BSD. В нем весь принадлежавший AT&T исходный код был либо заменен, либо удален, как в случае с шестью файлами, которые не так-то просто было переписать. При портировании кода Net/2 в код для архитектуры x86-32 Джолицы заново написали недостающие исходные файлы, и первый выпуск (версия 0.0) системы 386/BSD состоялся в феврале 1992 года.

После первой волны успеха и популярности работа над 386/BSD по различным причинам замедлилась. Вскоре появились две альтернативные группы разработчиков, которые создавали собственные выпуски на основе 386/BSD. Это были NetBSD, где основной упор был сделан на возможность портирования на широкий круг аппаратных платформ, и FreeBSD, созданный с прицелом на высокую производительность и получивший наиболее широкое распространение из всех современных версий BSD. Первый выпуск NetBSD под номером 0.8 состоялся в апреле 1993 года. Первый компакт-диск с FreeBSD (версии 1.0) появился в декабре 1993 года. Еще одна версия BSD под названием OpenBSD была выпущена в 1996 году (исходная версия вышла под номером 2.0) после ответвления от проекта NetBSD. В OpenBSD основное внимание уделялось безопасности. В середине 2003 года, после отделения от FreeBSD 4.x, появилась новая версия BSD — DragonFly BSD. Подход к ее разработке отличался от применявшегося при создании FreeBSD 5.x. Теперь особое внимание было уделено проектированию под архитектуры симметричной многопроцессорности (SMP).

Наверное, рассказ об истории BSD в начале 1990-х годов будет неполным без упоминания о судебных процессах между UNIX System Laboratories (USL, дочерней компании, принадлежащей AT&T и занимавшейся разработкой и рыночным продвижением UNIX) и командой из Беркли. В начале 1992 года компания Berkeley Software Design, Incorporated (BSDi, в настоящее время входит в состав Wind River) приступила к распространению сопровождаемых на коммерческой основе версий BSD UNIX под названием BSD/OS (на базе выпуска Net/2) и добавлений, разработанных Джолицами под названием 386/BSD. Компания BSDi распространяла двоичный и исходный код по цене $995 и советовала потенциальным клиентам пользоваться телефонным номером 1-800-ITS-UNIX.

В апреле 1992 года компания USL предъявила иск компании BSDi, пытаясь воспрепятствовать продаже этих проектов. Как заявлялось в USL, они по-прежнему представляли собой исходный код, который был защищен патентом, полученным USL, и составлял коммерческую тайну. Компания USL также потребовала, чтобы BSDi прекратила использовать вводящий в заблуждение телефонный номер. Со временем иск был выдвинут еще и Калифорнийскому университету. Суд в конечном итоге отклонил все, кроме двух претензий USL, а также встречный иск Калифорнийского университета к USL, в котором утверждалось, что USL не упомянула о том, что в System V содержится код BSD.

В ходе рассмотрения иска в суде USL была куплена компанией Novell, чей руководитель, ныне покойный Рэй Нурда (Ray Noorda), публично заявил, что он предпочел бы конкурировать на рынке, а не в суде. Спор окончательно был урегулирован в январе 1994 года. В итоге от Калифорнийского университета потребовали удалить из выпуска Net/2 три из 18 000 файлов, внести незначительные изменения в несколько файлов и добавить упоминание об авторских правах USL в отношении примерно 70 других файлов, которые университет тем не менее мог продолжать распространять на свободной основе. Эта измененная система была выпущена в июне 1994 года под названием 4.4BSD-Lite. (Последним выпуском университета в июне 1995 года был 4.4BSD-Lite, выпуск 2.) На данный момент по условиям правового урегулирования требуется, чтобы в BSDi, FreeBSD и NetBSD их база Net/2 была заменена исходным кодом 4.4BSD-Lite. Как отмечено в публикации [McKusick et al., 1996], хотя эти обстоятельства привели к замедлению процесса разработки версий, производных от BSD, был и положительный эффект. Он заключался в том, что эти системы были повторно синхронизированы с результатами трехлетней работы, проделанной университетской группой Computer Systems Research Group со времени выпуска Net/2.


Номера версий ядра Linux

Подобно большинству свободно распространяемых продуктов, для Linux практикуется модель ранних (release-early) и частых (release-often) выпусков, поэтому новые исправленные версии ядра появляются довольно часто (иногда чуть ли не каждый день). По мере расширения круга пользователей Linux каждая модель выпуска была настроена так, чтобы не влиять на тех, кто уже пользуется этой системой. В частности, после выпуска Linux 1.0 разработчики ядра приняли систему нумерации версий ядра x.y.z, где x обозначала номер основной версии, y — номер второстепенной версии в рамках основной версии, а z — номер пересмотра второстепенной версии (с незначительными улучшениями и исправлениями).

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

После выпуска версии ядра с номером 2.6 модель разработки была изменена. Главной причиной для этого изменения послужили проблемы и недовольства, вызванные длительными периодами между выпусками стабильных версий ядра[1]. Вокруг доработки этой модели периодически возникали споры, но основными остались следующие характеристики[2].

• Версии ядер перестали делить на стабильные и дорабатываемые. Каждый новый выпуск 2.6.z может содержать новые функции. У выпуска есть жизненный цикл, начинающийся с добавления функций, которые затем стабилизируются в течение нескольких версий-кандидатов. Когда такие версии признают достаточно стабильными, их выпускают в качестве ядра 2.6.z. Между циклами выпуска обычно проходит около трех месяцев.

• Иногда в стабильный выпуск с номером 2.6.z требуется внести небольшие исправления для устранения недостатков или решения проблем безопасности. Если эти исправления важны и кажутся достаточно простыми, то разработчики не ждут следующего выпуска с номером 2.6.z, а вносят их, выпуская версию с номером вида 2.6.z.r. Здесь r является следующим номером для второстепенной редакции ядра, имеющего номер 2.6.z.[3]

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

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

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


Портирование на другие аппаратные архитектуры

В начале разработки Linux главной целью было не достижение возможности портирования системы на другие вычислительные архитектуры, а создание работоспособной реализации под архитектуру Intel 80386. Но с ростом популярности Linux стала портироваться на другие архитектуры. Список аппаратных архитектур, на которые была портирована Linux, продолжает расти и включает в себя x86-64, Motorola/IBM PowerPC и PowerPC64, Sun SPARC и SPARC64 (UltraSPARC), MIPS, ARM (Acorn), IBM zSeries (бывшая System/390), Intel IA-64 (Itanium; см. публикацию [Mosberger & Eranian, 2002]), Hitachi SuperH, HP PA-RISC и Motorola 68000.


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

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

Самые первые дистрибутивы появились в 1992 году. Это были MCC Interim Linux (Manchester Computing Centre, UK), TAMU (Texas A&M University) и SLS (SoftLanding Linux System). Самый старый из выживших до сих пор коммерческих дистрибутивов, Slackware, появился в 1993 году. Примерно в то же время появился и некоммерческий дистрибутив Debian, за которым вскоре последовали SUSE и Red Hat. В настоящее время весьма большой популярностью пользуется дистрибутив Ubuntu, который впервые появился в 2004 году. Теперь многие компании-распространители также нанимают программистов, которые активно обновляют существующие проекты по разработке свободного ПО или инициируют новые проекты.


1.3. Стандартизация

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


1.3.1. Язык программирования C

К началу 1980-х годов язык C существовал уже в течение 10 лет и был реализован во множестве разнообразных UNIX-систем, а также в других операционных системах. В некоторых реализациях отмечались незначительные различия. В частности, это произошло из-за того, что определенные аспекты требуемого функционионала языка не были подробно описаны в существующем де-факто стандарте C. Этот стандарт приводился в вышедшей в 1978 году книге Кернигана (Kernighan) и Ритчи (Ritchie) «Язык программирования Cи». (Синтаксис языка C, описанный в этой книге, иногда называют традиционным C, или K&R C.) Кроме того, с появлением в 1985 году языка C++ проявились конкретные улучшения и дополнения, которые могли быть привнесены в C без нарушения совместимости с существующими программами. В частности, сюда можно отнести прототипы функций, присваивание структур, спецификаторы типов (const и volatile), перечисляемые типы и ключевое слово void.

Эти факторы побудили к стандартизации C. Ее кульминацией в 1989 году стало утверждение Американским институтом национальных стандартов (ANSI) стандарта языка C (X3.159-1989), который в 1990 году был принят в качестве стандарта (ISO/IEC 9899:1990) Международной организацией по стандартизации (ISO). Наряду с определением синтаксиса и семантики языка C в этом стандарте давалось описание стандартной библиотеки C, включающей возможности stdio, функции обработки строк, математические функции, различные файлы заголовков и т. д. Эту версию C обычно называют C89 или (значительно реже) ISO C90, и она полностью рассмотрена во втором издании (1988 года) книги Кернигана и Ритчи «Язык программирования Си».

Пересмотренное издание стандарта языка C было принято ISO в 1999 году (ISO/IEC 9899:1999; см. http://www.open-std.org/jtc1/sc22/wg14/www/standards). Его обычно называют C99, и он включает несколько изменений языка и его стандартной библиотеки. В частности, там описаны добавление типов данных long long и логического (булева), присущий C++ стиль комментариев (//), ограниченные указатели и массивы переменной длины. Новый стандарт для языка Си (ISO/IEC 9899:2011) опубликован 8 декабря 2011. В нем описаны поддержка многопоточности, обобщенные макросы, анонимные структуры и объединения, статичные утверждения, функция quick_exit, новый режим эксклюзивного открытия файла и др.

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

Исторически C89 часто называли ANSI C, и это название до сих пор иногда употребляется в таком значении. Например, оно используется в gcc, где спецификатор — ansi означает «поддерживать все программы ISO C90». Но мы будем избегать этого названия, поскольку теперь оно звучит несколько двусмысленно. После того как комитет ANSI принял пересмотренную версию C99, будет правильным считать, что стандартом ANSI C следует называть C99.


1.3.2. Первые стандарты POSIX

Термин POSIX (аббревиатура от Portable Operating System Interface) обозначает группу стандартов, разработанных под руководством Института инженеров электротехники и электроники — Institute of Electrical and Electronic Engineers (IEEE), а точнее, его комитета по стандартам для портируемых приложений — Portable Application Standards Committee (PASC, http://www.pasc.org/). Цель PASC-стандартов — содействие портируемости приложений на уровне исходного кода.

Название POSIX было предложено Ричардом Столлманом (Richard Stallman). Последняя буква X появилась потому, что названия большинства вариантов UNIX заканчивались на X. В стандарте указывалось, что название должно произноситься как pahzicks, наподобие слова positive («положительный»).

Для нас в стандартах POSIX наибольший интерес представляет первый стандарт POSIX, который назывался POSIX.1 (или в более полном виде POSIX 1003.1), и последующий стандарт POSIX.2.


POSIX.1 и POSIX.2

POSIX.1 стал IEEE-стандартом в 1988 году и после небольшого количества пересмотров был принят как стандарт ISO в 1990 году (ISO/IEC 9945-1:1990). (Полных версий POSIX нет в свободном доступе, но их можно приобрести у IEEE на сайте http://www.ieee.org/.)

POSIX.1 сначала основывался на более раннем (1984 года) неофициальном стандарте, выработанном объединением поставщиков UNIX под названием /usr/group.

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

POSIX.1 основан на системном вызове UNIX и API библиотечных функций языка C, но при этом не требует, чтобы с этим интерфейсом была связана какая-либо конкретная реализация. Это означает, что интерфейс может быть реализован любой операционной системой и не обязательно UNIX. Фактически некоторые поставщики добавили API к своим собственным операционным системам, сделав их совместимыми с POSIX.1, в то же время оставив сами ОС в практически неизменном виде.

Большое значение приобрели также расширения исходного стандарта POSIX.1. Стандарт IEEE POSIX 1003.1b (POSIX.1b, ранее называвшийся POSIX.4 или POSIX 1003.4), одобренный в 1993 году, содержит расширения базового стандарта POSIX для работы в режиме реального времени. Стандарт IEEE POSIX 1003.1c (POSIX.1c), одобренный в 1995 году, содержит определения потоков в POSIX. В 1996 году была разработана пересмотренная версия стандарта POSIX.1 (ISO/IEC 9945-1:1996), основной текст которой остался без изменений, но в него были внесены дополнения, касающиеся работы в режиме реального времени и использования потоков. Стандарт IEEE POSIX 1003.1g (POSIX.1g) определил API для работы в сети, включая сокеты. Стандарт IEEE POSIX 1003.1d (POSIX.1d), одобренный в 1999 году, и POSIX.1j, одобренный в 2000 году, определили дополнительные расширения основного стандарта POSIX для работы в режиме реального времени.

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

Родственный стандарт POSIX.2 (1992, ISO/IEC 9945-2:1993) затрагивает оболочку и различные утилиты UNIX, включая интерфейс командной строки компилятора кода языка C.


FIPS 151-1 и FIPS 151-2

FIPS является аббревиатурой от федерального стандарта обработки информации — Federal Information Processing Standard. Это название набора стандартов, разработанных правительством США и используемых гражданскими правительственными учреждениями. В 1989 году был опубликован стандарт FIPS 151-1, основанный на стандарте 1988 года IEEE POSIX.1 и предварительной версии стандарта ANSI C. Основное отличие FIPS 151-1 от POSIX.1 (1988 года) заключалось в том, что по стандарту FIPS требовалось наличие кое-каких функций, которые в POSIX.1 оставались необязательными. Поскольку основным покупателем компьютерных систем было правительство США, большинство поставщиков обеспечили совместимость своих UNIX-систем с FIPS 151-1-версией стандарта POSIX.1.

Стандарт FIPS 151-2 совмещен с редакцией 1990 ISO стандарта POSIX.1, но в остальном остался без изменений. Уже устаревший FIPS 151-2 был отменен в феврале 2000 года.


1.3.3. X/Open Company и Open Group

X/Open Company представляла собой консорциум, основанный международной группой поставщиков UNIX. Он предназначался для принятия и внедрения существующих стандартов с целью выработки всеобъемлющего согласованного набора стандартов открытых систем. Им было выработано руководство X/Open Portability Guide, состоящее из серий руководств по обеспечению портируемости на базе стандартов POSIX. Первым весомым выпуском этого руководства в 1989 году стал документ под названием Issue 3 (XPG3), за которым в 1992 году последовал документ XPG4. Последний был пересмотрен в 1994 году, в результате чего появился XPG4 версии 2, стандарт, который также включал в себя важные части документа AT&T’s System V Interface Definition Issue 3. Эта редакция также была известна как Spec 1170, где число 1170 соответствует количеству интерфейсов (функций, файлов заголовков и команд), определенных стандартом.

Когда компания Novell, которая в начале 1993 года приобрела у AT&T бизнес, связанный с системами UNIX, позже самоустранилась от него, она передала права на торговую марку UNIX консорциуму X/Open. (Планы по этой передаче были анонсированы в 1993 году, но в силу юридических требований передача прав была отложена до начала 1994 года.) Стандарт XPG4 версии 2 был перекомпонован в единую UNIX-спецификацию — Single UNIX Specification (SUS, иногда встречается вариант SUSv1), которая также известна под названием UNIX 95. Эта перекомпоновка включала XPG4 версии 2, спецификацию X/Open Curses Issue 4 версии 2 и спецификацию X/Open Networking Services (XNS) Issue 4. Версия 2 Single UNIX Specification (SUSv2, http://www.unix.org/version2/online.html) появилась в 1997 году, а реализация UNIX, сертифицированная на соответствие требованиям этой спецификации, может называть себя UNIX 98. (Данный стандарт иногда также называют XPG5.)

В 1996 году консорциум X/Open объединился с Open Software Foundation (OSF), в результате чего был сформирован консорциум The Open Group. В настоящее время в The Open Group, где продолжается разработка стандартов API, входят практически все компании или организации, имеющие отношение к системам UNIX.

OSF был одним из двух консорциумов поставщиков, сформировавшихся в ходе UNIX-войн в конце 1980-х годов. Кроме прочих, в него входили Digital, IBM, HP, Apollo, Bull, Nixdorf и Siemens. OSF был сформирован главным образом в ответ на угрозы, вызванные бизнес-альянсом AT&T (изобретателей UNIX) и Sun (наиболее мощного игрока на рынке рабочих станций под управлением UNIX). В свою очередь, AT&T, Sun и другие компании сформировали конкурирующий консорциум UNIX International.


1.3.4. SUSv3 и POSIX.1-2001

Начиная с 1999 года IEEE, Open Group и ISO/IEC Joint Technical Committee 1 объединились в Austin Common Standards Revision Group (CSRG, http://www.opengroup.org/austin/) с целью пересмотра и утверждения стандартов POSIX и Single UNIX Specification. (Свое название Austin Group получила потому, что ее первое заседание состоялось в городе Остин, штат Техас, в сентябре 1998 года.) В результате этого в декабре 2001 года был одобрен стандарт POSIX 1003.1-2001, иногда называемый просто POSIX.1-2001 (который впоследствии был утвержден в качестве ISO-стандарта ISO/IEC 9945:2002).

POSIX 1003.1-2001 заменил собой SUSv2, POSIX.1, POSIX.2 и ряд других более ранних стандартов POSIX. Этот стандарт также известен как Single UNIX Specification версии 3, и ссылки на него в книге будут в основном иметь вид SUSv3.

Базовая спецификация SUSv3 состоит почти из 3700 страниц, разбитых на следующие четыре части.

• Base Definitions (XBD). Включает в себя определения, термины, положения и спецификации содержимого файлов заголовков. Всего предоставляются спецификации 84 файлов заголовков.

System Interfaces (XSH). Начинается с различной полезной справочной информации. В основном в ней содержатся спецификации разных функций (реализуемых либо в виде системных вызовов, либо в виде библиотечных функций в конкретной реализации UNIX). Всего в нее включено 1123 системных интерфейса.

Shell and Utilities (XCU). В этой части определяются возможности оболочки и различные команды (утилиты) UNIX. Всего в ней представлено 160 утилит.

• Rationale (XRAT). Включает в себя текстовые сведения и объяснения, касающиеся предыдущих частей.

Кроме того, в SUSv3 входит спецификация X/Open CURSES Issue 4 версии 2 (XCURSES), в которой определяются 372 функции и три файла заголовков для API curses, предназначенного для управления экраном.

Всего в SUSv3 описано 1742 интерфейса. Для сравнения, в POSIX.1-1990 (с FIPS 151-2) определено 199 интерфейсов, а в POSIX.2-1992 — 130 утилит.

Спецификация SUSv3 доступна по адресу http://www.unix.org/version3/online.html. Реализации UNIX, сертифицированные в соответствии с требованиями SUSv3, имеют право называться UNIX 03.

В результате проблем, обнаруженных с момента одобрения исходного текста SUSv3, в него были внесены различные незначительные правки и уточнения. В итоге появилось техническое исправление номер 1 (Technical Corrigendum Number 1), уточнения из которого были внесены в редакцию SUSv3 от 2003 года, и техническое исправление номер 2 (Technical Corrigendum Number 2), уточнения из которого добавлены в редакцию 2004 года.


POSIX-соответствие, XSI-соответствие и XSI-расширение

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

С некоторыми нюансами эти различия сохраняются в POSIX 1003.1-2001, являющемся одновременно стандартом IEEE и Open Group Technical Standard (то есть, как уже было отмечено, он представляет собой объединение раннего POSIX и SUS). Этот документ определяет два уровня соответствия.

• Соответствие POSIX: задает основной уровень интерфейсов, который должен предоставляться реализацией, претендующей на соответствие. Допускает предоставление реализацией других необязательных интерфейсов.

• Соответствие X/Open System Interface (XSI): чтобы соответствовать XSI, реализация должна отвечать всем требованиям соответствия POSIX, а также предоставлять ряд интерфейсов и особенностей поведения, которые считаются для него необязательными. Реализация должна достичь этого уровня, чтобы получить от Open Group право называться UNIX 03.

Дополнительные интерфейсы и особенности поведения, требуемые для XSI-соответствия, обобщенно называются XSI-расширением. В их число входит поддержка потоков, функций mmap() и munmap(), API dlopen, ограничений ресурсов, псевдотерминалов, System V IPC, API syslog, функции poll(), учетных записей пользователей.

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

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


Неопределенные и слабо определенные интерфейсы

Временами вам будут попадаться ссылки на неопределенные или слабо определенные в SUSv3 интерфейсы.

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

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

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


Средства с пометкой LEGACY

Иногда какое-то средство в SUSv3 имеет пометку LEGACY. Она означает, что это средство оставлено для сохранения совместимости со старыми приложениями, а в новых приложениях его лучше не использовать. Во многих случаях существуют другие API, предоставляющие эквивалентные функциональные возможности.


1.3.5. SUSv4 и POSIX.1-2008

В 2008 году Austin Group завершила пересмотр объединенной спецификации POSIX.1 и Single UNIX. Как и предшествующая версия стандарта, она состоит из основной спецификации, дополненной XSI-расширением. Эту редакцию мы будем называть SUSv4.

Изменения в SUSv4 не столь масштабные, как в SUSv3. Из наиболее существенных можно выделить следующие.

• Добавлены новые спецификации для некоторых функций. Из их числа в книге упоминаются dirfd(), fdopendir(), fexecve(), futimens(), mkdtemp(), psignal(), strsignal() и utimensat(). Другие новые функции предназначены для работы с файлами (например, openat(), рассматриваемая в разделе 18.11) и практически являются аналогами существующих функций (например, open()). Они отличаются лишь тем, что относительный путь к файлу разрешается относительно каталога, на который ссылается дескриптор открытого файла, а не относитльно текущего рабочего каталога процесса.

• Некоторые функции, указанные в SUSv3 как необязательные, становятся обязательной частью стандарта в SUSv4. Например, отдельные функции, составлявшие в SUSv3 часть XSI-расширения, в SUSv4 стали частью базового стандарта. Среди функций, ставших обязательными в SUSv4, можно назвать функции, входящие в API сигналов режима реального времени (раздел 22.8), в API POSIX-таймеров (раздел 23.6), в API dlopen (раздел 42.1) и в API POSIX-семафоров (глава 48).

• Кое-какие функции из SUSv3 в SUSv4 помечены как устаревшие. К их числу относятся asctime(), ctime(), ftw(), gettimeofday(), getitimer(), setitimer() и siginterrupt().

• Спецификации некоторых функций, помеченных в SUSv3 как устаревшие, из SUSv4 удалены. Среди них gethostbyname(), gethostbyaddr() и vfork().

• Различные особенности существующих в SUSv3 спецификаций претерпели изменения в SUSv4. Например, к списку функций, от которых требуется обеспечение безопасной обработки асинхронных сигналов, добавились дополнительные функции (см. табл. 21.1).

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


1.3.6. Этапы развития стандартов UNIX

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

Ситуация с сетевыми стандартами была несколько сложнее. Действия по стандартизации в этой области начали предприниматься в конце 1980-х годов. В то время был образован комитет POSIX 1003.12 для стандартизации API сокетов, API X/Open Transport Interface (XTI) (альтернативный API программирования сетевых приложений на основе интерфейса транспортного уровня System V Transport Layer Interface) и различных API, связанных с работой в сети. Становление стандарта POSIX 1003.12 заняло несколько лет, в течение которых он был переименован в POSIX 1003.1g. Этот стандарт был одобрен в 2000 году.

Параллельно с разработкой POSIX 1003.1g в X/Open велась разработка спецификации X/Open Networking Specification (XNS). Первая ее версия, XNS, выпуск 4, была частью первой версии Single UNIX Specification. За ней последовала спецификация XNS, выпуск 5, которая составила часть SUSv2. По сути, XNS, выпуск 5, была такой же, как и текущая на то время предварительная версия (6.6) POSIX.1g. Затем последовала спецификация XNS, выпуск 5.2, отличавшаяся от XNS, выпуск 5, и был одобрен стандарт POSIX.1g. В нем был помечен устаревшим API XTI и включен обзор протокола Internet Protocol version 6 (IPv6), разработанного в середине 1990-х годов. XNS, выпуск 5.2, заложил основу для документации, относящейся к работе в сети и включенной в замененный нынче стандарт SUSv3. По аналогичным причинам POSIX.1g был отозван в качестве стандарта вскоре после своего одобрения.



Рис. 1.1. Взаимоотношения между различными стандартами UNIX и C


1.3.7. Стандарты реализаций

В дополнение к стандартам, разработанным независимыми компаниями, иногда даются ссылки на два стандарта реализаций, определенных финальным выпуском BSD (4.4BSD) и AT&T’s System V, выпуск 4 (SVR4). Последний стандарт реализации был документально оформлен публикацией System V Interface Definition (SVID) (компания AT&T). В 1989 году AT&T опубликовала выпуск 3 стандарта SVID, определявшего интерфейс, который должна предоставлять реализация UNIX, чтобы называться System V, выпуск 4. (В Интернете SVID можно найти по адресу http://www.sco.com/developers/devspecs/.)

Поскольку поведение некоторых системных вызовов и библиотечных функций в SVR4 и BSD различается, во многих реализациях UNIX предусмотрены библиотеки совместимости и средства условной компиляции. Они эмулируют поведение тех особенностей, которые не были включены в конкретную реализацию UNIX (см. подраздел 3.6.1). Тем самым облегчается портирование приложений из другой реализации UNIX.


1.3.8. Linux, стандарты и нормативная база Linux

В первую очередь разработчики Linux (то есть ядра, библиотеки glibc и инструментария) стремятся соответствовать различным стандартам UNIX, особенно POSIX и Single UNIX Specification. Но на время написания этих строк ни один из распространителей Linux не получил от Open Group права называться UNIX. Проблемы — во времени и средствах. Чтобы получить такое право, каждому дистрибутиву от поставщика необходимо пройти тестирование на соответствие, и с выпуском каждого нового дистрибутива требуется повторное тестирование. Тем не менее близкое соблюдение различных стандартов позволяет Linux успешно оставаться на рынке UNIX.

Что касается большинства коммерческих реализаций UNIX, разработкой и распространением операционной системы занимается одна и та же компания. А вот с Linux картина иная — реализация отделена от распространения, и распространением Linux занимаются многие организации: как коммерческие, так и некоммерческие.

Линус Торвальдс не занимается распространением или поддержкой какого-либо конкретного дистрибутива Linux. Но в отношении других отдельных разработчиков Linux ситуация иная. Многие разработчики, занимающиеся ядром Linux и другими проектами свободного программного обеспечения, являются сотрудниками различных компаний-распространителей Linux или работают на компании (такие как IBM и HP), испытывающие большой интерес к Linux. Хотя эти компании могут влиять на направление, в котором развивается Linux, выделяя программистам время на разработку конкретных проектов, ни одна из них не управляет операционной системой Linux как таковой. И конечно же, многие другие разработчики ядра Linux и GNU-проектов трудятся на добровольной основе.

На время написания этих строк Торвальдс числился сотрудником фонда Linux Foundation (http://www.linux-foundation.org/), бывшей лаборатории Open Source Development Laboratory, OSDL, некоммерческого консорциума организаций, уполномоченных оказывать содействие развитию Linux.

Из-за наличия нескольких распространителей Linux, а также из-за того, что реализаторы ядра не контролируют содержимое дистрибутивов, «стандартной» коммерческой версии Linux не существует. Ядро отдельного дистрибутива Linux обычно базируется на некоторой версии ядра Linux из основной ветки разработки (которую ведет Торвальдс) с набором необходимых изменений.

В этих изменениях (патчах) предоставляются функции, которые в той или иной степени считаются коммерчески востребованными и способными тем самым обеспечить конкурентные преимущества на рынке. Иногда эти исправления принимаются в качестве основной ветви разработки. Фактически некоторые новые функции ядра изначально были разработаны компаниями-распространителями и, прежде чем стать частью основной ветки, появились в их дистрибутивах. Например, версия 3 журналируемой файловой системы Reiserfs была частью ряда дистрибутивов Linux задолго до того, как была принята в качестве основной ветки версии 2.4.

Итогом всех ранее упомянутых обстоятельств стали в основном незначительные различия в системах, предлагаемых разными компаниями-распространителями Linux. Это напоминает расхождения в реализациях в начальные годы существования UNIX, но в существенно меньших масштабах. В результате усилий по обеспечению совместимости между разными дистрибутивами Linux появился стандарт под названием Linux Standard Base (LSB) (http://www.linux-foundation.org/en/LSB). В рамках LSB был разработан и внедрен набор стандартов для систем Linux. Они обеспечивают возможность запуска двоичных приложений (то есть скомпилированных программ) на любой LSB-совместимой системе.

Двоичная портируемость, внедренная с помощью LSB, отличается от портируемости исходного кода, внедренной стандартом POSIX. Портируемость исходного кода означает возможность написания программы на языке C с ее последующей успешной компиляцией и запуском на любой POSIX-совместимой системе. Двоичная совместимость имеет куда более привередливый характер, и, как правило, она недостижима на различных аппаратных платформах. Она позволяет осуществлять однократную компиляцию на конкретной аппаратной платформе, после чего запускать откомпилированную программу в любой совместимой реализации, запущенной на этой аппаратной платформе. Двоичная портируемость является весьма важным требованием для коммерческой жизнеспособности приложений, созданных под Linux независимыми поставщиками программных продуктов — independent software vendor (ISV).


1.4. Резюме

Впервые система UNIX была введена в эксплуатацию в 1969 году на мини-компьютере Digital PDP-7 Кеном Томпсоном из Bell Laboratories (подразделения AT&T). Множество идей было привнесено из ранее созданной системы MULTICS. К 1973 году UNIX была перенесена на мини-компьютер PDP-11 и переписана на C, языке программирования, разработанном и реализованном в Bell Laboratories Деннисом Ритчи (Dennis Ritchie). По закону не имея возможности продавать UNIX, компания AT&T за символическую плату распространяла полноценную систему среди университетов. Дистрибутив включал исходный код и стал весьма популярен в университетской среде. Это была недорогая операционная система, код которой можно было изучать и изменять как преподавателям, так и студентам, изучающим компьютерные технологии.

Ключевую роль в разработке UNIX сыграл Калифорнийский институт в Беркли. Там операционная система была расширена Кеном Томпсоном и несколькими студентами-выпускниками. К 1979 году университет создал собственный UNIX-дистрибутив под названием BSD. Он получил широкое распространение в академических кругах и стал основой для нескольких коммерческих реализаций.

Тем временем компания AT&T лишилась своего монопольного положения на рынке и занялась продажами системы UNIX. В результате появился еще один основной вариант UNIX под названием System V, который также послужил базой для нескольких коммерческих реализаций.

К разработке (GNU) Linux привели два разных проекта. Одним из них был GNU-проект, основанный Ричардом Столлманом. В конце 1980-х годов в рамках GNU была создана практически завершенная, свободно распространяемая реализация UNIX. Недоставало только работоспособного ядра. В 1991 году Линус Торвальдс, вдохновленный ядром Minix, придуманным Эндрю Таненбаумом, создал работоспособное ядро UNIX для архитектуры Intel x86-32. Торвальдс обратился за помощью к другим программистам для усовершенствования ядра. На его призыв откликнулось множество программистов, и со временем система Linux была расширена и портирована на большое количество разнообразных аппаратных архитектур.

Проблемы портирования, возникшие из-за наличия разных реализаций UNIX и C, существовавших к концу 1980-х годов, сильно повлияли на решение вопросов стандартизации. Язык C прошел стандартизацию в 1989 году (C89), а пересмотренный стандарт вышел в 1999 году (C99). Первая попытка стандартизации интерфейса операционной системы привела к выпуску POSIX.1, одобренному в качестве стандарта IEEE в 1988 году и в качестве стандарта ISO в 1990 году. В 1990-е годы были разработаны дополнительные стандарты, включая различные версии спецификации Single UNIX Specification. В 2001 году был одобрен объединенный стандарт POSIX 1003.1-2001 и SUSv3. Этот стандарт собрал воедино и расширил различные более ранние стандарты POSIX и более ранние версии спецификации Single UNIX Specification. В 2008 году был завершен менее масштабный пересмотр стандарта, что привело к объединенному стандарту POSIX 1003.1-2008 и SUSv4.

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


Дополнительная информация

Дополнительные сведения об истории и стандартах UNIX можно найти в публикациях [Ritchie, 1984], [McKusick et al., 1996], [McKusick & Neville-Neil, 2005], [Libes & Ressler, 1989], [Garfinkel et al., 2003], [Stevens & Rago, 2005], [Stevens, 1999], [Quartermann & Wilhelm, 1993], [Goodheart & Cox, 1994] и [McKusick, 1999].

В публикации [Salus, 1994] изложена подробная история UNIX, откуда и были почерпнуты основные сведения, приведенные в начале главы. В публикации [Salus, 2008] предоставлена краткая история Linux и других проектов по созданию свободных программных продуктов. Многие подробности истории UNIX можно также найти в выложенной в Интернет книге Ронды Хобен (Ronda Hauben) History of UNIX (http://www.dei.isep.ipp.pt/~acc/docs/unix.html). Весьма подробную историческую справку, относящуюся к выпускам различных реализаций UNIX, вы найдете по адресу http://www.levenez.com/unix/.

В публикации [Josey, 2004] дается обзорная информация по истории систем UNIX и разработке SUSv3, а также приводится руководство по использованию спецификации, сводные таблицы имеющихся в SUSv3 интерфейсов и пособие по переходу от SUSv2 к SUSv3 и от C89 к C99.

Наряду с предоставлением программных продуктов и документации, на сайте GNU (http://www.gnu.org/) содержится подборка философских статей, касающихся свободного программного обеспечения. А в публикации [Williams, 2002] дается биография Ричарда Столлмана.

Собственные взгляды Торвальдса на развитие Linux можно найти в публикации [Torvalds & Diamond, 2001].

2. Основные понятия

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


2.1. Основа операционной системы: ядро

Понятие «операционная система» зачастую употребляется в двух различных значениях.

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

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

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

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

Исполняемая программа ядра Linux обычно находится в каталоге с путевым именем /boot/vmlinuz или же в другом подобном ему каталоге. Происхождение этого имени имеет исторические корни. В ранних реализациях UNIX ядро называлось unix. В более поздних реализациях UNIX, работающих с виртуальной памятью, ядро было переименовано в vmunix. В Linux в имени файла отобразилось название системы, а вместо последней буквы x использована буква z. Это говорит о том, что ядро является сжатым исполняемым файлом.


Задачи, выполняемые ядром

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

• Диспетчеризация процессов. У компьютера имеется один или несколько центральных процессоров (CPU), выполняющих инструкции программ. Как и другие UNIX-системы, Linux является многозадачной операционной системой с вытеснением. Многозадачность означает, что несколько процессов (например, запущенные программы) могут одновременно находиться в памяти и каждая может получить в свое распоряжение центральный процессор (процессоры). Вытеснение означает, что правила, определяющие, какие именно процессы получают в свое распоряжение центральный процессор (ЦП) и на какой срок, устанавливает имеющийся в ядре диспетчер процессов (а не сами процессы).

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

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

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

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

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

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

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

• Предоставление интерфейса прикладного программирования (API) системных вызовов. Процессы могут запрашивать у ядра выполнение различных задач с использованием точек входа в ядро, известных как системные вызовы. API системных вызовов Linux — главная тема данной книги. Этапы выполнения процессом системного вызова подробно описаны в разделе 3.1.

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


Режим ядра и пользовательский режим

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

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


Сравнение взглядов на систему со стороны процессов и со стороны ядра

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

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

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

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


Дополнительная информация

В число современных публикаций, охватывающих концепции и конструкции операционных систем с конкретными ссылками на системы UNIX, входят труды [Tanenbaum, 2007], [Tanenbaum & Woodhull, 2006] и [Vahalia, 1996]. В последнем подробно описаны архитектуры виртуальной памяти. Издание [Goodheart & Cox, 1994] предоставляет подробную информацию, касающуюся System V Release 4. Публикация [Maxwell, 1999] содержит аннотированный перечень избранных частей ядра Linux 2.2.5. В издании [Lions, 1996] представлен детально разобранный исходный код Sixth Edition UNIX, который и сегодня остается полезным источником информации о внутреннем устройстве UNIX. В публикации [Bovet & Cesati, 2005] дается описание реализации ядра Linux 2.6.


2.2. Оболочка

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

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

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

• Bourne shell (sh). Эта оболочка, написанная Стивом Борном (Steve Bourne), является старейшей из широко используемых оболочек. Она была стандартной оболочкой для Seventh Edition UNIX. Bourne shell характеризуется множеством особенностей, актуальных для всех оболочек: перенаправление ввода-вывода, организация конвейеров, генерация имен файлов (подстановка), использование переменных, работа с переменными среды, подстановка команд, фоновое выполнение команд и функций. Все последующие реализации UNIX включали Bourne shell в дополнение к любым другим оболочкам, которые они могли предоставлять.

C shell (csh). Была написана Биллом Джоем (Bill Joy) из Калифорнийского университета в Беркли. Такое имя она получила из-за схожести многих конструкций управления выполнением этой оболочки с конструкциями языка программирования C. Оболочка C shell предоставляет ряд полезных интерактивных средств, недоступных в Bourne shell, включая историю команд, управление заданиями и использование псевдонимов. Оболочка C shell не имеет обратной совместимости с Bourne shell. Хотя стандартной интерактивной оболочкой на BSD была C shell, сценарии оболочки (которые вскоре будут рассмотрены) обычно создавались для Bourne shell, дабы сохранялась их портируемость между всеми реализациями UNIX.

Korn shell (ksh). Оболочка была написана в качестве преемника Bourne shell Дэвидом Корном (David Korn) из AT&T Bell Laboratories. Кроме поддержки обратной совместимости с Bourne shell, в нее были включены интерактивные средства, подобные предоставляемым оболочкой C shell.

Bourne again shell (bash). Была разработана в рамках проекта GNU в качестве усовершенствованной реализации Bourne shell. Она предоставляет интерактивные средства, подобные тем, что доступны при работе с оболочками C и Korn. Основными создателями bash являются Брайан Фокс (Brian Fox) и Чет Рэми (Chet Ramey). Bash, наверное, наиболее популярная оболочка Linux. (Фактически в Linux Bourne shell, sh, предоставляется посредством имеющейся в bash наиболее приближенной к оригиналу эмуляции оболочки sh.)

В POSIX.2-1992 определяется стандарт для оболочки, которая была основана на актуальной в ту пору версии оболочки Korn. В наши дни стандарту POSIX соответствуют обе оболочки: и Korn shell и bash, но при этом они предоставляют несколько расширений стандарта и отличаются друг от друга многими из этих расширений.

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

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


2.3. Пользователи и группы

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


Пользователи

У каждого пользователя имеется уникальное имя для входа в систему (имя пользователя) и соответствующий числовой идентификатор пользователя — numeric user ID (UID). Каждому пользователю соответствует своя строка в файле паролей системы, /etc/passwd, где прописаны эти сведения, а также следующая дополнительная информация.

• Идентификатор группы (Group ID, GID) — числовой идентификатор группы, к которой принадлежит пользователь.

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

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

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


Группы

В целях администрирования, в частности для управления доступом к файлам и другим системным ресурсам, есть смысл собрать пользователей в группы. Например, всех специалистов в команде, работающей над одним проектом и пользующейся по этой причине одним и тем же набором файлов, можно свести в одну группу. В ранних реализациях UNIX пользователь мог входить только в одну группу. В версии BSD пользователю позволялось одновременно принадлежать сразу нескольким группам, и эта идея была подхвачена создателями других реализаций UNIX, а также поддержана стандартом POSIX.1-1990. Каждая группа обозначается одной строкой в системном файле групп, /etc/group, включающем следующую информацию.

• Название группы — уникальное имя группы.

Идентификатор группы (Group ID, GID) — числовой идентификатор, связанный с данной группой.

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


Привилегированный пользователь

Один из пользователей, называемый привилегированным (superuser), имеет в системе особые привилегии. У учетной записи привилегированного пользователя UID содержит значение 0, и, как правило, в качестве имени пользователя применяется слово root. В обычных системах UNIX привилегированный пользователь обходит в системе все разрешительные проверки. Таким образом, к примеру, привилегированный пользователь может получить доступ к любому файлу в системе независимо от требуемых для этого разрешений и может отправлять сигналы любому имеющемуся в системе пользовательскому процессу. Системный администратор пользуется учетной записью привилегированного пользователя для выполнения различных задач администрирования системы.


2.4. Иерархия одного каталога. Что такое каталоги, ссылки и файлы

Для организации всех файлов в системе ядро поддерживает структуру одного иерархического каталога. (В отличие от таких операционных систем, как Microsoft Windows, где своя собственная иерархия каталогов имеется у каждого дискового устройства.) Основу этой иерархии составляет корневой каталог по имени / (слеш). Все файлы и каталоги являются дочерними или более отдаленными потомками корневого каталога. Пример такой иерархической файловой структуры показан на рис. 2.1.



Рис. 2.1. Пример иерархии одного каталога Linux


Типы файлов

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

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


Каталоги и ссылки

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

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

Каждый каталог содержит как минимум две записи:. (точка), которая представляет собой ссылку на сам каталог, и… (точка-точка), которая является ссылкой на его родительский каталог — тот каталог, что расположен над ним в иерархии. Каждый каталог, за исключением корневого, имеет свой родительский каталог. Для корневого каталога запись… является ссылкой на него самого (таким образом, обозначение /.. — то же самое, что и /).


Символьные ссылки

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

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


Имена файлов

В большинстве файловых систем Linux длина имен файлов может составлять до 255 символов. Имена файлов могут содержать любые символы, за исключением слешей (/) и символа с нулевым кодом (\0). Но желательно использовать только буквы и цифры, а также символы точки (.), подчеркивания (_) и дефиса (-). Этот 65-символьный набор, [-._a-zA-Z0-9], в SUSv3 называется портируемым набором символов для имен файлов.

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

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


Путевые имена

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

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

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

• Абсолютное путевое имя начинается со слеша и указывает на местоположение файла относительно корневого каталога. Примеры абсолютного путевого имени для файлов, показанных на рис. 2.1: /home/mtk/.bashrc, /usr/include и / (путевое имя корневого каталога).

Относительное путевое имя указывает местоположение файла относительно рабочего каталога текущего запущенного процесса (см. ниже) и отличается от абсолютного путевого имени отсутствием начального слеша. На рис. 2.1 из каталога usr на файл types.h можно указать, используя относительное путевое имя include/sys/types.h, а из каталога avr доступ к файлу. bashrc можно получить с помощью относительного путевого имени../mtk/.bashrc.


Текущий рабочий каталог

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

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


Владение файлами и права доступа

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

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

• права доступа на чтение позволяют считывать содержимое файла;

права доступа на запись дают возможность вносить изменения в содержимое файла;

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

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

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

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

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


2.5. Модель файлового ввода-вывода

Одной из отличительных черт модели ввода-вывода в системах UNIX является понятие универсальности ввода-вывода. Это означает, что одни и те же системные вызовы (open(), read(), write(), close() и т. д.) используются для выполнения ввода-вывода во всех типах файлов, включая устройства. (Ядро преобразует запросы приложений на ввод/вывод в соответствующие операции файловой системы или драйверов устройств, выполняющие ввод/вывод в отношении целевого файла или устройства.) Из этого следует, что программа, использующая эти системные вызовы, будет работать с любым типом файлов.

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

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


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

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

При запуске оболочкой процесс наследует, как правило, три дескриптора открытых файлов:

• дескриптор 0 является стандартным вводом — файлом, из которого процесс получает свой ввод;

• дескриптор 1 является стандартным выводом — файлом, в который процесс записывает свой вывод;

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

В интерактивной оболочке или программе эти три дескриптора подключены, как правило, к терминалу. В библиотеке stdio они соответствуют файловым потокам stdin, stdout и stderr.


Библиотека stdio

Для выполнения файлового ввода-вывода программы обычно используют функции ввода-вывода, содержащиеся в стандартной библиотеке языка C. Этот набор функций, известный как библиотека stdio, включает функции fopen(), fclose(), scanf(), printf(), fgets(), fputs() и т. д. Функции stdio наслаиваются поверх системных вызовов ввода-вывода (open(), close(), read(), write() и т. д.).

Предполагается, что читатель уже знаком со стандартными функциями ввода-вывода (stdio) языка C, поэтому мы не рассматриваем их в данной книге. Дополнительные сведения о библиотеке stdio можно найти в изданиях [Kernighan & Ritchie, 1988], [Harbison & Steele, 2002], [Plauger, 1992] и [Stevens & Rago, 2005].


2.6. Программы

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


Фильтры

Понятие «фильтр» часто обозначает программу, которая считывает вводимые в нее данные из stdin, выполняет преобразования этого ввода и записывает преобразованные данные на stdout. Примеры фильтров: cat, grep, tr, sort, wc, sed и awk.


Аргументы командной строки

В языке C программы могут получать доступ к аргументам командной строки — словам, введенным в командную строку при запуске программы. Для доступа к аргументам командной строки глобальная функция main() программы объявляется следующим образом:

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

Переменная argc содержит общее количество аргументов командной строки, а отдельные аргументы доступны в виде строковых значений, которые нужно указать в качестве элементов массива argv. Первая из этих строк, argv[0], соответствует имени самой программы.


2.7. Процессы

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

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


Модель памяти процесса

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

• Текст — инструкции программы.

Данные — статические переменные, используемые программой.

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

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


Создание процесса и выполнение программы

Процесс может создать новый процесс с помощью системного вызова fork(). Процесс, вызывающий fork(), известен как родительский процесс, а новый процесс называется дочерним процессом. Ядро создает дочерний процесс путем изготовления дубликата родительского процесса. Дочерний процесс наследует копии родительских сегментов данных, стека и кучи, которые затем могут изменяться независимо от своих родительских копий. (Текст программы размещается в области памяти с пометкой «только для чтения» и совместно используется двумя процессами.)

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

У вызова execve() есть ряд надстроек в виде родственных функций библиотеки языка C с несколько отличающимся интерфейсом, но сходной функциональностью. У всех этих функций имена начинаются со строки exec. (В тех случаях, когда разница между ними неважна, мы будем для общей ссылки на эти функции использовать обозначение exec(). И все же следует иметь в виду, что на самом деле функции по имени exec() не существует.)

В основном глагол «выполнять» (exec) будет употребляться для описания операций, выполняемых execve() и ее библиотечными функциями-надстройками.


Идентификатор процесса и идентификатор родительского процесса

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


Завершение процесса и код завершения

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

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


Принадлежащие процессу идентификаторы пользователя и группы (учетные данные)

У каждого процесса имеется несколько связанных с ним идентификаторов пользователей (UID) и групп (GID). К ним относятся следующие.

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

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

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


Привилегированные процессы

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

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


Мандаты (возможности)

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

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

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


Процесс init

При загрузке системы ядро создает особый процесс, который называется init, «родитель всех процессов». Он ведет свое происхождение от программного файла /sbin/init. Все процессы в системе создаются (используя fork()) либо процессом init, либо одним из его потомков. Процесс init всегда имеет идентификатор процесса 1 и запускается с правами доступа суперпользователя. Процесс init не может быть уничтожен (даже привилегированным пользователем) и завершается только при завершении работы системы. Основной задачей init является создание и слежение за процессами, требуемыми работающей системе. (Подробности можно найти на странице руководства init(8).)


Процессы-демоны

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

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

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

К примерам процессов-демонов относятся syslogd, который записывает сообщения в системный журнал, и httpd, который обслуживает веб-страницы посредством протокола передачи гипертекста — Hypertext Transfer Protocol (HTTP).


Список переменных среды

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

Переменные среды, как в следующем примере, создаются в большинстве оболочек командой export (или командой setenv в оболочке C shell):

$ export MYVAR='Hello world'

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

Программы на языке C могут получать доступ к среде, используя внешнюю переменную (char **environ) и различные библиотечные функции, позволяющие процессу извлекать и изменять значения в его среде.

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


Ограничения ресурсов

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

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

Ограничения ресурсов оболочки могут быть отрегулированы с использованием команды ulimit (limit в оболочке C shell). Эти настройки ограничений наследуются дочерними процессами, создаваемыми оболочкой для выполнения команд.


2.8. Отображение в памяти

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

Отображения делятся на две категории.

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

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

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

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

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


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

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


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

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

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

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


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

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

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


2.10. Межпроцессное взаимодействие и синхронизация

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

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

• сигналы, которые используются в качестве признака возникновения события;

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

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

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

очереди сообщений, которые используются для обмена сообщениями (пакетами данных) между процессами;

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

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

Широкое разнообразие IPC-механизмов в системах UNIX с иногда перекрываемыми функциональными возможностями частично объясняется их различием в отдельных вариантах UNIX-систем и требованиями со стороны различных стандартов. Например, FIFO-буферы и доменные сокеты UNIX, по сути, выполняют одну и ту же функцию, позволяющую неродственным процессам в одной и той же системе осуществлять обмен данными. Их совместное существование в современных системах UNIX объясняется тем, что FIFO-буферы пришли из System V, а сокеты были взяты из BSD.


2.11. Сигналы

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

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

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

• пользователь набрал на клавиатуре команду прерывания (обычно это Ctrl+C);

• завершился один из дочерних процессов данного процесса;

• истекло время таймера (будильника), установленного процессом;

• процесс попытался получить доступ к неверному адресу в памяти.

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

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

• игнорирует сигнал;

• прекращает свою работу по сигналу;

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

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

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


2.12. Потоки

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

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


2.13. Группы процессов и управление заданиями в оболочке

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

$ ls — l | sort — k5n | less

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

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


2.14. Сессии, управляющие терминалы и управляющие процессы

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

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

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

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

В любой момент времени одна из групп процессов в сессии является приоритетной группой (приоритетным заданием), которая может считывать ввод с терминала и отправлять на него вывод. Если пользователь набирает на управляющем терминале символ прерывания (обычно это Ctrl+C) или символ приостановки (обычно это Ctrl+Z), драйвер терминала отправляет сигнал, уничтожающий или приостанавливающий приоритетную группу процессов. У сессии может быть любое количество фоновых групп процессов (фоновых заданий), создаваемых с помощью символа амперсанда (&) в конце командной строки.

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


2.15. Псевдотерминалы

Псевдотерминалом называется пара подключенных виртуальных устройств, называемых ведущим (master) и ведомым (slave). Эта пара устройств предоставляет IPC-канал, позволяющий перемещать данные в обоих направлениях между двумя устройствами.

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

Псевдотерминалы используются в различных приложениях, в первую очередь в реализациях окон терминала, предоставляемых при входе в систему X Window, и в приложениях, предоставляющих сервисы входа в сеть, например telnet и ssh.


2.16. Дата и время

Для процесса интерес представляют два типа времени.

• Реальное время, которое измеряется либо относительно некоторой стандартной точки (календарного времени), либо относительно какой-то фиксированной точки, обычно от начала жизненного цикла процесса (истекшее или физическое время). В системах UNIX календарное время измеряется в секундах, прошедших с полуночи 1 января 1970 года всемирного координированного времени — Universal Coordinated Time (обычно сокращаемого до UTC), и координируется на базовой точке часовых поясов, определяемой линией долготы, проходящей через Гринвич, Великобритания. Эта дата, близкая к дате появления системы UNIX, называется началом отсчета времени (Epoch).

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

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


2.17. Клиент-серверная архитектура

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

Клиент-серверное приложение разбито на два составляющих процесса:

• клиент, который просит сервер о какой-либо услуге, отправив ему сообщение с запросом;

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

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

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

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

Серверы могут реализовывать различные сервисы, например:

• предоставление доступа к базе данных или другому совместно используемому информационному ресурсу;

• предоставление доступа к удаленному файлу по сети;

• инкапсуляция какой-нибудь бизнес-логики;

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

• обслуживание веб-страниц.

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

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

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

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


2.18. Выполнение действий в реальном масштабе времени

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

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

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

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

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


2.19. Файловая система /proc

Как и в некоторых других реализациях UNIX, в Linux предоставляется файловая система /proc, состоящая из набора каталогов и файлов, смонтированных в каталоге /proc.

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

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

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


2.20. Резюме

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

3. Общее представление о системном программировании

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

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

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


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

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

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

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

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

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

С точки зрения программирования, инициирование системного вызова во многом похоже на вызов функции языка C. Но при выполнении системного вызова многое происходит закулисно. Чтобы пояснить, рассмотрим все последовательные этапы происходящего на конкретной аппаратной реализации — x86-32.

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

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

3. Поскольку вход в ядро всеми системными вызовами осуществляется одинаково, ядру нужен какой-нибудь метод идентификации системного вызова. Для обеспечения такой возможности функция-оболочка копирует номер системного вызова в конкретный регистр (%eax).

4. В функции-оболочке выполняется машинный код системного прерывания (int 0x80), заставляющий процессор переключиться из пользовательского режима в режим ядра и выполнить код, указатель на который расположен в векторе прерывания системы 0x80 (в десятичной системе счисления — 128).

В более современных архитектурах x86-32 реализуется инструкция sysenter, предоставляющая более быстрый способ входа в режим ядра, по сравнению с обычной инструкцией системного прерывания int 0x80. Использование sysenter поддерживается в версии ядра 2.6 и в glibc, начиная с версии 2.3.2.

5. В ответ на системное прерывание 0x80 ядро для его обработки инициирует свою подпрограмму system_call() (которая находится в ассемблерном файле arch/x86/kernel/entry.S). Обработчик прерывания делает следующее;

1) сохраняет значения регистров в стеке ядра (см. раздел 6.5);

2) проверяет допустимость номера системного вызова;

3) вызывает соответствующую подпрограмму обслуживания системного вызова. Ее поиск ведется по номеру системного вызова: в таблице всех подпрограмм обслуживания системных вызовов в качестве индекса используется номер системного вызова (переменная ядра sys_call_table). Если у подпрограммы обслуживания системного вызова имеются аргументы, то она сначала проверяет их допустимость. Например, она проверяет, что адреса указывают на места, допустимые в пользовательской памяти. Затем подпрограмма обслуживания системного вызова выполняет требуемую задачу, которая может предполагать изменение значений адресов, указанных в переданных аргументах, и перемещение данных между пользовательской памятью и памятью ядра (например, в операциях ввода-вывода). И наконец, подпрограмма обслуживания системного вызова возвращает подпрограмме system_call() код возврата;

4) восстанавливает значения регистров из стека ядра и помещает в стек возвращаемое значение системного вызова;

5) возвращает управление функции-оболочке, одновременно переводя процессор в пользовательский режим.

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

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

Это соглашение основано на предположении, что подпрограммы обслуживания системных вызовов в случае успеха не возвращают отрицательных значений. Но для некоторых таких подпрограмм это предположение неверно. Обычно это не вызывает никаких проблем, поскольку диапазон превращенных в отрицательные числа значений errno не пересекается с допустимыми отрицательными возвращаемыми значениями. Тем не менее в одном случае все же появляется проблема — когда дело касается операции F_GETOWN системного вызова fcntl(), рассматриваемого в разделе 59.3.

На рис. 3.1 на примере системного вызова execve() показана описанная выше последовательность. В Linux/x86-32 execve() является системным вызовом под номером 11 (__NR_execve). Следовательно, 11-я запись в векторе sys_call_table содержит адрес sys_execve(), подпрограммы, обслуживающей этот системный вызов. (В Linux подпрограммы обслуживания системных вызовов обычно имеют имена в формате sys_xyz(), где xyz() является соответствующим системным вызовом.)



Рис. 3.1. Этапы выполнения системного вызова


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

В качестве примера издержек на осуществление системного вызова рассмотрим системный вызов getppid(). Он просто возвращает идентификатор родительского процесса, которому принадлежит вызывающий процесс. В одной из принадлежащих автору книги x86-32-систем с запущенной Linux 2.6.25 на совершение 10 миллионов вызовов getppid() ушло приблизительно 2,2 секунды. То есть на каждый вызов ушло около 0,3 микросекунды. Для сравнения, на той же системе на 10 миллионов вызовов функции языка C, которая просто возвращает целое число, ушло 0,11 секунды, или около 1/12 времени, затраченного на вызовы getppid(). Разумеется, большинство системных вызовов имеет более существенные издержки, чем getppid().

Поскольку с точки зрения программы на языке C вызов функции-оболочки библиотеки C является синонимом запуска соответствующей подпрограммы обслуживания системного вызова, далее в книге для обозначения действия «вызов функции-оболочки, которая запускает системный вызов xyz()» будет использоваться фраза «запуск системного вызова xyz()».

Дополнительные сведения о механизме системных вызовов Linux можно найти в изданиях [Love, 2010], [Bovet & Cesati, 2005] и [Maxwell, 1999].


3.2. Библиотечные функции

Библиотечная функция — одна из множества функций, составляющих стандартную библиотеку языка C. (Для краткости далее в книге при упоминании о конкретной функции вместо словосочетания «библиотечная функция» будем просто использовать слово «функция».) Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т. д.

Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами. Например, библиотечная функция fopen() использует для открытия файла системный вызов open(). Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова. Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а системный вызов write() просто выводит блок байтов. Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().


3.3. Стандартная библиотека языка C; GNU-библиотека C (glibc)

В различных реализациях UNIX существуют разные версии стандартной библиотеки языка C. Наиболее часто используемой реализацией в Linux является GNU-библиотека языка C (glibc, http://www.gnu.org/software/libc/).

Первоначально основным разработчиком и специалистом по обслуживанию GNU-библиотеки C был Роланд Макграт (Roland McGrath). До 2012 года этим занимался Ульрих Дреппер (Ulrich Drepper), после чего его полномочия были переданы сообществу разработчиков, многие из которых перечислены на странице https://sourceware.org/glibc/wiki/MAINTAINERS.

Для Linux доступны и другие реализации/версии библиотеки C, среди которых есть и такие, которым требуется (относительно) небольшой объем памяти, а предназначены они для встраиваемых приложений. В качестве примера можно привести uClibc (http://www.uclibc.org/) и diet libc (http://www.fefe.de/dietlibc/). В данной книге мы ограничиваемся рассмотрением glibc, поскольку именно эта библиотека языка C используется большинством приложений, разработанных под Linux.


Определение версии glibc в системе

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

$ /lib/libc.so.6

GNU C Library stable release version 2.10.1, by Roland McGrath et al.

Copyright (C) 2009 Free Software Foundation, Inc.

This is free software; see the source for copying conditions.

There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A

PARTICULAR PURPOSE.

Compiled by GNU CC version 4.4.0 20090506 (Red Hat 4.4.0–4).

Compiled on a Linux >>2.6.18-128.4.1.el5<< system on 2009-08-19.

Available extensions:

The C stubs add-on version 2.1.2.

crypt add-on version 2.1 by Michael Glad and others

GNU Libidn by Simon Josefsson

Native POSIX Threads Library by Ulrich Drepper et al

BIND-8.2.3-T5B

RT using linux kernel aio

For bug reporting instructions, please see:

<http://www.gnu.org/software/libc/bugs.html>.

В некоторых дистрибутивах Linux GNU-библиотека C находится в другом месте, путь к которому отличается от /lib/libc.so.6. Один из способов определить местоположение библиотеки — выполнить программу ldd (list dynamic dependencies — список динамических зависимостей) в отношении исполняемого файла, имеющего динамические ссылки на glibc (ссылки такого рода имеются у большинства исполняемых файлов). Затем можно изучить полученный в результате этого список библиотечных зависимостей, чтобы найти местоположение совместно используемой библиотеки glibc:

$ ldd myprog | grep libc

libc.so.6 => /lib/tls/libc.so.6 (0x4004b000)

Есть два средства, с помощью которых прикладная программа может определить версию библиотеки GNU C в системе: тестирование констант или вызов библиотечной функции. Начиная с версии 2.0, в glibc определяются две константы, __GLIBC__ и __GLIBC_MINOR__, которые могут быть протестированы в ходе компиляции (в инструкциях #if). В системе с установленной glibc 2.12 эти константы будут иметь значения 2 и 12. Но использование этих констант не принесет пользы в программе, скомпилированной в одной системе, но запущенной в другой системе с отличающейся по версии библиотекой glibc. Учитывая вышесказанное, можно воспользоваться функцией gnu_get_libc_version() для определения версии glibc доступной во время исполнения программы.

#include <gnu/libc-version.h>


const char *gnu_get_libc_version(void);

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

Функция gnu_get_libc_version() возвращает указатель на строку вида 2.12.

Информацию о версии можно также получить, воспользовавшись функцией confstr() для извлечения значения конфигурационной переменной (относящегося к конкретной glibc-библиотеке) _CS_GNU_LIBC_VERSION. В результате вызова функции будет возвращена строка вида glibc 2.12.


3.4. Обработка ошибок, возникающих при системных вызовах и вызовах библиотечных функций

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

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

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


Обработка ошибок системных вызовов

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

fd = open(pathname, flags, mode); /* Системный вызов для открытия файла */

if (fd == -1) {

/* Код для обработки ошибки */

}

if (close(fd) == -1) {

/* Код для обработки ошибки */

}

При неудачном завершении системного вызова для глобальной целочисленной переменной errno устанавливается положительное значение, позволяющее идентифицировать конкретную ошибку. Объявление errno, а также набора констант для различных номеров ошибок предоставляется за счет включения заголовочного файла <errno.h>. Все относящиеся к ошибкам символьные имена начинаются с E. Список возможных значений errno, которые могут быть возвращены каждым системным вызовом, предоставляется на каждой странице руководства в разделе заголовочного файла ERRORS. Простой пример использования errno для обнаружения ошибки системного вызова имеет следующий вид:

cnt = read(fd, buf, numbytes);

if (cnt == -1) {

if (errno == EINTR)

fprintf(stderr, "read was interrupted by a signal\n");

// чтение было прервано сигналом

else {

/* Произошла какая-то другая ошибка */

}

}

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

Некоторые системные вызовы (например, getpriority()) могут вполне законно возвращать при успешном завершении значение –1. Чтобы определить, не возникла ли при таких вызовах ошибка, перед самим вызовом нужно установить errno в 0 и проверить ее значение после вызова. Если вызов возвращает –1, а errno имеет ненулевое значение, значит, произошла ошибка. (Это же правило применимо к некоторым библиотечным функциям.)

Общая линия поведения после неудачного системного вызова заключается в выводе сообщения об ошибке на основе значения переменной errno. Для этой цели предоставляются библиотечные функции perror() и strerror().

Функция perror() выводит строку, указываемую с помощью аргумента msg. За строкой следует сообщение, соответствующее текущему значению переменной errno.

#include <stdio.h>


void perror(const char *msg);

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

fd = open(pathname, flags, mode);

if (fd == -1) {

perror("open");

exit(EXIT_FAILURE);

}

Функция strerror() возвращает строку описания ошибки, соответствующую номеру ошибки, который задан в ее аргументе errnum.

#include <string.h>


char *strerror(int errnum);

Возвращает указатель на строку с описанием ошибки, соответствующую значению errnum

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

Если в errnum указан нераспознаваемый номер ошибки, то strerror() возвращает строку вида Unknown error nnn (неизвестная ошибка с таким-то номером). В некоторых других реализациях strerror() в таких случаях возвращает значение NULL.

Поскольку функции perror() и strerror() чувствительны к настройкам локали (см. раздел 10.4), описания ошибок выводятся на языке локали.


Обработка ошибок из библиотечных функций

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

• Некоторые библиотечные функции возвращают информацию об ошибке точно таким же образом, что и системные вызовы: возвращают значение –1, а значение переменной errno указывает на конкретную ошибку. Примером такой функции может послужить remove(), удаляющая файл (используя системный вызов unlink()) или каталог (используя системный вызов rmdir()). Ошибки из этих функций могут определяться точно так же, как и ошибки из системных вызовов.

• Некоторые библиотечные функции возвращают при ошибке значение, отличное от –1, но все-таки устанавливают значение для переменной errno, чтобы указать на конкретные условия возникновения ошибки. Например, функция fopen() в случае ошибки возвращает нулевой указатель и устанавливает для переменной errno значение в зависимости от того, какой из положенных в основу ее работы системных вызовов завершился неудачно. Для определения типа таких ошибок могут применяться функции perror() и strerror().

• Другие библиотечные функции вообще не используют переменную errno. Метод определения наличия и причин ошибок зависит от конкретной функции и задокументирован на посвященной ей странице руководства. Для таких функций применение errno, perror() или strerror() с целью определения типа ошибок будет неприемлемо.


3.5. Пояснения по поводу примеров программ, приводимых в книге

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


3.5.1. Ключи и аргументы командной строки

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

Традиционные ключи командной строки UNIX состоят из начального дефиса, буквы, идентифицирующей ключ, и необязательного аргумента. (Утилиты GNU предоставляют расширенный синтаксис ключей, состоящий из двух начальных дефисов, за которыми следует строка, идентифицирующая ключ, и необязательный аргумент.) Для анализа этих ключей используется стандартная библиотечная функция getopt().

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


3.5.2. Типовые функции и заголовочные файлы

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


Типовой заголовочный файл

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


Листинг 3.1. Заголовочный файл, используемый в большинстве примеров программ

lib/tlpi_hdr.h

#ifndef TLPI_HDR_H

#define TLPI_HDR_H /* Предотвращает случайное двойное включение */


#include <sys/types.h> /* Определения типов, используемые

многими программами */

#include <stdio.h> /* Стандартные функции ввода-вывода */

#include <stdlib.h> /* Прототипы наиболее востребованных библиотечных

функций плюс константы EXIT_SUCCESS

и EXIT_FAILURE */

#include <unistd.h> /* Прототипы многих системных вызовов */

#include <errno.h> /* Объявление errno и определение констант ошибок */

#include <string.h> /* Наиболее используемые функции обработки строк */

#include "get_num.h" /* Объявление наших функций для обработки числовых

аргументов (getInt(), getLong()) */

#include "error_functions.h" /* Объявление наших функций обработки ошибок */

typedef enum { FALSE, TRUE } Boolean;


#define min(m,n) ((m) < (n)? (m): (n))

#define max(m,n) ((m) > (n)? (m): (n))

#endif

lib/tlpi_hdr.h


Функции определения типа ошибок

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


Листинг 3.2. Объявление для наиболее востребованных функций обработки ошибок

lib/error_functions.h

#ifndef ERROR_FUNCTIONS_H

#define ERROR_FUNCTIONS_H


void errMsg(const char *format…);


#ifdef __GNUC__

/* Этот макрос блокирует предупреждения компилятора при использовании

команды 'gcc — Wall', жалующиеся, что "control reaches end of non-void

function", то есть что управление достигло конца функции, которая

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

прекращения выполнения main() или какой-нибудь другой функции,

которая должна вернуть значение определенного типа (не void) */

#define NORETURN __attribute__ ((__noreturn__))

#else

#define NORETURN

#endif


void errExit(const char *format…) NORETURN;

void err_exit(const char *format…) NORETURN;

void errExitEN(int errnum, const char *format…) NORETURN;

void fatal(const char *format…) NORETURN;

void usageErr(const char *format…) NORETURN;

void cmdLineErr(const char *format…) NORETURN;


#endif

lib/error_functions.h

Для определения типа ошибок системных вызовов и библиотечных функций используются функции errMsg(), errExit(), err_exit() и errExitEN().

#include "tlpi_hdr.h"


void errMsg(const char *format…);

void errExit(const char *format…);

void err_exit(const char *format…);

void errExitEN(int errnum, const char *format…);

Функция errMsg() выводит сообщение на стандартное устройство вывода ошибки. Ее список аргументов совпадает со списком для функции printf(), за исключением того, что в строку вывода автоматически добавляется символ конца строки. Функция errMsg() выводит текст ошибки, соответствующий текущему значению переменной errno. Этот текст состоит из названия ошибки, например EPERM, дополненного описанием ошибки в том виде, в котором его возвращает функция strerror(), а затем следует вывод, отформатированный согласно переданным агрументам.

По своему действию функция errExit() похожа на errMsg(), но она также прекращает выполнение программы, либо вызвав функцию exit(), либо, если переменная среды EF_DUMPCORE содержит непустое строковое значение, вызвав функцию abort(), чтобы создать файл дампа ядра для его использования отладчиком. (Файлы дампа ядра будут рассмотрены в разделе 22.1.)

Функция err_exit() похожа на errExit(), но имеет два отличия:

• не сбрасывает стандартный вывод перед выводом в него сообщения об ошибке;

• завершает процесс путем вызова _exit(), а не exit(). Это приводит к тому, что процесс завершается без сброса буферов stdio или вызова обработчиков выхода.

Подробности этих различий в работе err_exit() станут понятнее при изучении главы 25, где рассматривается разница между _exit() и exit(), а также обработка буферов stdio и обработчики выхода в дочернем процессе, созданном с помощью fork(). А пока мы просто возьмем на заметку, что функция err_exit() будет особенно полезна при написании нами библиотечной функции, создающей дочерний процесс, который следует завершить по причине возникновения ошибки. Это завершение должно произойти без сброса дочерней копии родительских буферов stdio (то есть буферов вызывающего процесса) и без вызова обработчиков выхода, созданных родительским процессом.

Функция errExitEN() представляет собой практически то же самое, что и errExit(), за исключением того, что вместо сообщения об ошибке, характерного текущему значению errno, она выводит текст, соответствующий номеру ошибки (отсюда и суффикс EN), заданному в аргументе errnum.

В основном функция errExitEN() применяется в программах, использующих API потоков стандарта POSIX. В отличие от традиционных системных вызовов UNIX, возвращающих при возникновении ошибки –1, функции потоков стандарта POSIX позволяют определить тип ошибки по ее номеру, возвращенному в качестве результата их выполнения (то есть в errno, как правило, помещается положительный номер типа). (В случае успеха функции потоков стандарта POSIX возвращают 0.)

Определить типы ошибок из функции потоков стандарта POSIX можно с помощью следующего кода:

errno = pthread_create(&thread, NULL, func, &arg);

if (errno!= 0)

errExit("pthread_create");

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

int s;

s = pthread_create(&thread, NULL, func, &arg);

if (s!= 0)

errExitEN(s, "pthread_create");

Согласно терминологии языка C левостороннее выражение (lvalue) — это выражение, ссылающееся на область хранилища3. Наиболее характерным его примером является идентификатор для переменной. Некоторые операторы также выдают такие выражения. Например, если p является указателем на область хранилища, то *p является левосторонним выражением. Согласно API потоков стандарта POSIX, errno переопределяется в функцию, возвращающую указатель на область хранилища, относящуюся к отдельному потоку (см. раздел 31.3).

Для определения других типов ошибок используются функции fatal(), usageErr() и cmdLineErr().

#include "tlpi_hdr.h"


void fatal(const char *format…);

void usageErr(const char *format…);

void cmdLineErr(const char *format…);

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

Функция usageErr() предназначена для определения типов ошибок при использовании аргументов командной строки. Она принимает список аргументов в стиле printf() и выводит строку Usage:, за которой следует отформатированный вывод на стандартное устройство вывода ошибки, после чего она завершает выполнение программы путем вызова exit(). (Некоторые примеры программ в этой книге предоставляют свою собственную расширенную версию функции usageErr() под именем usageError().)

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

Реализации функций определения типов ошибок показаны в листинге 3.3.


Листинг 3.3. Функции обработки ошибок, используемые всеми программами

lib/error_functions.c

#include <stdarg.h>

#include "error_functions.h"

#include "tlpi_hdr.h"

#include "ename.c.inc" /* Определяет ename и MAX_ENAME */


#ifdef __GNUC__

__attribute__ ((__noreturn__))

#endif


static void

terminate(Boolean useExit3)

{

char *s;


/* Сохраняет дамп ядра, если переменная среды EF_DUMPCORE определена

и содержит непустую строку; в противном случае вызывает exit(3)

или _exit(2), в зависимости от значения 'useExit3'. */

s = getenv("EF_DUMPCORE");

if (s!= NULL && *s!= '\0')

abort();

else if (useExit3)

exit(EXIT_FAILURE);

else

_exit(EXIT_FAILURE);

}


static void

outputError(Boolean useErr, int err, Boolean flushStdout,

const char *format, va_list ap)

{

#define BUF_SIZE 500

char buf[BUF_SIZE], userMsg[BUF_SIZE], errText[BUF_SIZE];

vsnprintf(userMsg, BUF_SIZE, format, ap);


if (useErr)

snprintf(errText, BUF_SIZE, " [%s %s]",

(err > 0 && err <= MAX_ENAME)?

ename[err]: "?UNKNOWN?", strerror(err));

else

snprintf(errText, BUF_SIZE, ":");

snprintf(buf, BUF_SIZE, "ERROR%s %s\n", errText, userMsg);

if (flushStdout)

fflush(stdout); /* Сброс всего ожидающего стандартного вывода */

fputs(buf, stderr);

fflush(stderr); /* При отсутствии построчной буферизации в stderr */

}


void

errMsg(const char *format…)

{

va_list argList;

int savedErrno;

savedErrno = errno; /* В случае ее изменения на следующем участке */

va_start(argList, format);

outputError(TRUE, errno, TRUE, format, argList);

va_end(argList);

errno = savedErrno;

}


void

errExit(const char *format…)

{

va_list argList;

va_start(argList, format);

outputError(TRUE, errno, TRUE, format, argList);

va_end(argList);

terminate(TRUE);

}


void

err_exit(const char *format…)

{

va_list argList;

va_start(argList, format);

outputError(TRUE, errno, FALSE, format, argList);

va_end(argList);

terminate(FALSE);

}


void

errExitEN(int errnum, const char *format…)

{

va_list argList;

va_start(argList, format);

outputError(TRUE, errnum, TRUE, format, argList);

va_end(argList);

terminate(TRUE);

}


void

fatal(const char *format…)

{

va_list argList;

va_start(argList, format);

outputError(FALSE, 0, TRUE, format, argList);

va_end(argList);

terminate(TRUE);

}


void

usageErr(const char *format…)

{

va_list argList;

fflush(stdout); /* Сброс всего ожидающего стандартного вывода */

fprintf(stderr, "Usage: ");

va_start(argList, format);

vfprintf(stderr, format, argList);

va_end(argList);

fflush(stderr); /* При отсутствии построчной буферизации в stderr */

exit(EXIT_FAILURE);

}


void

cmdLineErr(const char *format…)

{

va_list argList;

fflush(stdout); /* Сброс всего ожидающего стандартного вывода */

fprintf(stderr, "Command-line usage error: ");

va_start(argList, format);

vfprintf(stderr, format, argList);

va_end(argList);

fflush(stderr); /* При отсутствии построчной буферизации в stderr */

exit(EXIT_FAILURE);

}

lib/error_functions.c

Файл ename.c.inc, подключенный в листинге 3.3, показан в листинге 3.4. В этом файле определен массив строк ename, содержащий символьные имена, соответствующие каждому возможному значению errno. Наши функции обработки ошибок используют этот массив для вывода символьного имени, соответствующего конкретному номеру ошибки. Это выход из ситуации, при которой, с одной стороны, строка, возвращенная strerror(), не идентифицирует символьную константу, соответствующую ее сообщению об ошибке, в то время как, с другой стороны, на страницах руководства дается описание ошибок с использованием их символьных имен. По символьному имени на страницах руководства можно легко найти причину возникновения ошибки.

Содержимое файла ename.c.inc конкретизировано под архитектуру, поскольку значения errno в различных аппаратных архитектурах Linux несколько различаются. Версия, показанная в листинге 3.4, предназначена для системы Linux 2.6/x86-32. Этот файл был создан с использованием сценария (lib/Build_ename.sh), включенного в исходный код дистрибутива для данной книги. Сценарий можно использовать для создания версии ename.c.inc, которая должна подойти для конкретной аппаратной платформы и версии ядра.

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

В файле ename.c.inc мы можем увидеть, что у ошибок EAGAIN и EWOULDBLOCK одно и то же значение. (В SUSv3 на этот счет есть явно выраженное разрешение, и значения этих констант одинаковы в большинстве, но не во всех других системах UNIX.) Эти ошибки возвращаются системным вызовом в тех случаях, когда он должен быть заблокирован (то есть вынужден находиться в режиме ожидания, прежде чем завершить свою работу), но вызывающий код потребовал, чтобы системный вызов вместо входа в режим блокировки вернул ошибку. Ошибка EAGAIN появилась в System V и возвращалась системными вызовами, выполняющими ввод/вывод, операции с семафорами, операции с очередями сообщений и блокировку файлов (fcntl()). Ошибка EWOULDBLOCK появилась в BSD и возвращалась блокировкой файлов (flock()) и системными вызовами, связанными с сокетами.

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


Листинг 3.4. Имена ошибок Linux (для версии x86-32)

lib/ename.c.inc

static char *ename[] = {

/* 0 */ "",

/* 1 */ "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",

/* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EAGAIN/EWOULDBLOCK", "ENOMEM",

/* 13 */ "EACCES", "EFAULT", "ENOTBLK", "EBUSY", "EEXIST", "EXDEV",

/* 19 */ "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE", "EMFILE",

/* 25 */ "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS",

/* 31 */ "EMLINK", "EPIPE", "EDOM", "ERANGE", "EDEADLK/EDEADLOCK",

/* 36 */ "ENAMETOOLONG", "ENOLCK", "ENOSYS", "ENOTEMPTY", "ELOOP", "",

/* 42 */ "ENOMSG", "EIDRM", "ECHRNG", "EL2NSYNC", "EL3HLT", "EL3RST",

/* 48 */ "ELNRNG", "EUNATCH", "ENOCSI", "EL2HLT", "EBADE", "EBADR",

/* 54 */ "EXFULL", "ENOANO", "EBADRQC", "EBADSLT", "", "EBFONT",

"ENOSTR",

/* 61 */ "ENODATA", "ETIME", "ENOSR", "ENONET", "ENOPKG", "EREMOTE",

/* 67 */ "ENOLINK", "EADV", "ESRMNT", "ECOMM", "EPROTO", "EMULTIHOP",

/* 73 */ "EDOTDOT", "EBADMSG", "EOVERFLOW", "ENOTUNIQ", "EBADFD",

/* 78 */ "EREMCHG", "ELIBACC", "ELIBBAD", "ELIBSCN", "ELIBMAX",

/* 83 */ "ELIBEXEC", "EILSEQ", "ERESTART", "ESTRPIPE", "EUSERS",

/* 88 */ "ENOTSOCK", "EDESTADDRREQ", "EMSGSIZE", "EPROTOTYPE",

/* 92 */ "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT",

/* 95 */ "EOPNOTSUPP/ENOTSUP", "EPFNOSUPPORT", "EAFNOSUPPORT",

/* 98 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH",

/* 102 */ "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS", "EISCONN",

/* 107 */ "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT",

/* 111 */ "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH", "EALREADY",

/* 115 */ "EINPROGRESS", "ESTALE", "EUCLEAN", "ENOTNAM", "ENAVAIL",

/* 120 */ "EISNAM", "EREMOTEIO", "EDQUOT", "ENOMEDIUM", "EMEDIUMTYPE",

/* 125 */ "ECANCELED", "ENOKEY", "EKEYEXPIRED", "EKEYREVOKED",

/* 129 */ "EKEYREJECTED", "EOWNERDEAD", "ENOTRECOVERABLE", "ERFKILL"

};


#define MAX_ENAME 132

lib/ename.c.inc


Функции для анализа числовых аргументов командной строки

Заголовочный файл в листинге 3.5 содержит объявление двух функций, часто используемых для анализа целочисленных аргументов командной строки: getInt() и getLong(). Главное преимущество использования этих функций вместо atoi(), atol() и strtol() заключается в том, что они предоставляют основные средства проверки на допустимость числовых аргументов.

#include "tlpi_hdr.h"


int getInt(const char *arg, int flags, const char *name);

long getLong(const char *arg, int flags, const char *name);

Обе возвращают значение arg, преобразованное в число

Функции getInt() и getLong() преобразуют строку, на которую указывает параметр arg, в значение типа int или long соответственно. Если arg не содержит допустимый строковый образ целого числа (то есть не состоит только лишь из цифр и символов + и —), эта функция выводит сообщение об ошибке и завершает выполнение программы.

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

Аргумент flags предоставляет возможность управления работой функций getInt() и getLong(). Изначально они ожидают получения строк, содержащих десятичные целые числа со знаком. Путем логического сложения (|) в аргументе flags нескольких констант вида GN_*, определенных в листинге 3.5, можно выбрать иную основу для преобразования и ограничить диапазон чисел неотрицательными значениями или значениями больше нуля.


Листинг 3.5. Заголовочный файл для get_num.c

lib/get_num.h

#ifndef GET_NUM_H

#define GET_NUM_H

#define GN_NONNEG 01 /* Значение должно быть >= 0 */

#define GN_GT_0 02 /* Значение должно быть > 0 */


/* По умолчанию целые числа являются десятичными */

#define GN_ANY_BASE 0100 /* Можно использовать любое основание –

наподобие strtol(3) */

#define GN_BASE_8 0200 /* Значение выражено в виде восьмеричного числа */

#define GN_BASE_16 0400 /* Значение выражено в виде шестнадцатеричного числа */

long getLong(const char *arg, int flags, const char *name);


int getInt(const char *arg, int flags, const char *name);


#endif

lib/get_num.h

Реализации функций getInt() и getLong() показаны в листинге 3.6.

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


Листинг 3.6. Функции для анализа числовых аргументов командной строки

lib/get_num.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <limits.h>

#include <errno.h>

#include "get_num.h"


static void

gnFail(const char *fname, const char *msg, const char *arg, const char *name)

{

fprintf(stderr, "%s error", fname);

if (name!= NULL)

fprintf(stderr, " (in %s)", name);

fprintf(stderr, ": %s\n", msg);

if (arg!= NULL && *arg!= '\0')

fprintf(stderr, " offending text: %s\n", arg);

exit(EXIT_FAILURE);

}


static long

getNum(const char *fname, const char *arg, int flags, const char *name)

{

long res;

char *endptr;

int base;

if (arg == NULL || *arg == '\0')

gnFail(fname, "null or empty string", arg, name);

base = (flags & GN_ANY_BASE)? 0: (flags & GN_BASE_8)? 8:

(flags & GN_BASE_16)? 16: 10;

errno = 0;

res = strtol(arg, &endptr, base);

if (errno!= 0)

gnFail(fname, "strtol() failed", arg, name);

if (*endptr!= '\0')

gnFail(fname, "nonnumeric characters", arg, name);

if ((flags & GN_NONNEG) && res < 0)

gnFail(fname, "negative value not allowed", arg, name);

if ((flags & GN_GT_0) && res <= 0)

gnFail(fname, "value must be > 0", arg, name);

return res;

}


long

getLong(const char *arg, int flags, const char *name)

{

return getNum("getLong", arg, flags, name);

}

int

getInt(const char *arg, int flags, const char *name)

{

long res;

res = getNum("getInt", arg, flags, name);

if (res > INT_MAX || res < INT_MIN)

gnFail("getInt", "integer out of range", arg, name);

return (int) res;

}

lib/get_num.c


3.6. Вопросы переносимости

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


3.6.1. Макросы проверки возможностей

Поведение API системных вызовов и вызовов библиотечных функций регулируется различными стандартами (см. раздел 1.3). Одни стандарты определены организациями стандартизации, такими как Open Group (Single UNIX Specification), а другие — двумя исторически важными реализациями UNIX: BSD и System V, выпуск 4 (и объединенным System V Interface Definition).

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

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

#define _BSD_SOURCE 1

В качестве альтернативы можно воспользоваться ключом — D компилятора языка C:

$ cc — D_BSD_SOURCE prog.c

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

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

• _POSIX_SOURCE — если он задан (с любым значением), то предоставляет определения, соответствующие POSIX.1-1990 и ISO C (1990). Этот макрос заменен макросом _POSIX_C_SOURCE.

• _POSIX_C_SOURCE — если он определен со значением 1, то он производит такой же эффект, что и макрос _POSIX_SOURCE. Если он задан со значением большим или равным 199309, то также предоставляет определения для POSIX.1b (работа с системами реального времени). Если он приведен со значением, большим или равным 199506, то он также предоставляет определения для POSIX.1c (работа с потоками). Если он задан со значением 200112, то также предоставляет определения для базовой спецификации POSIX.1-2001 (то есть с включением XSI-расширения). (До выхода версии 2.3.3 заголовки glibc не интерпретировали значение 200112 для _POSIX_C_SOURCE.) Если макрос приведен со значением 200809, то он также предоставляет определения для базовой спецификации POSIX.1-2008. (До выхода версии 2.10 заголовочные файлы glibc не интерпретировали значение 200809 для _POSIX_C_SOURCE.)

• _XOPEN_SOURCE — если он задан (с любым значением), то предоставляет определения, соответствующие POSIX.1, POSIX.2 и X/Open (XPG4). Если он приведен со значением 500 или выше, то также предоставляет определения расширений SUSv2 (UNIX 98 и XPG5). Присвоение значения 600 или выше дополнительно приводит к предоставлению определения расширений SUSv3 XSI (UNIX 03) и расширений C99. (До выхода версии 2.2 заголовки glibc не интерпретировали значение 600 для _XOPEN_SOURCE.) Задание значения 700 или выше также приводит к предоставлению определения расширений SUSv4 XSI. (До выхода версии 2.10 заголовки glibc не интерпретировали значение 700 для _XOPEN_SOURCE.) Значения 500, 600 и 700 для _XOPEN_SOURCE были выбраны потому, что SUSv2, SUSv3 и SUSv4 являются соответственно выпусками Issues 5, 6 и 7 спецификаций X/Open.

Для glibc предназначены следующие макросы проверки возможностей.

• _BSD_SOURCE — если он задан (с любым значением), то предоставляет определения, соответствующие BSD. Явная установка одного лишь этого макроса приводит к тому, что в случае редких конфликтов стандартов предпочтение отдается определениям, соответствующим BSD.

• _SVID_SOURCE — если макрос приведен (с любым значением), то он предоставляет определения System V Interface Definition (SVID).

• _GNU_SOURCE — если он задан (с любым значением), то предоставляет все определения, предусмотренные предыдущими макросами, а также определения различных GNU-расширений.

Когда компилятор GNU C вызывается без специальных ключей, то по умолчанию определяются _POSIX_SOURCE, _POSIX_C_SOURCE=200809 (200112 с glibc версий от 2.5 до 2.9 или 199506 с glibc версии ниже 2.4), _BSD_SOURCE и _SVID_SOURCE.

Если определены отдельные макросы или компилятор вызван в одном из стандартных режимов (например, cc — ansi или cc — std=c99), то предоставляются только запрошенные определения. Существует одно исключение: если _POSIX_C_SOURCE не задан каким-либо другим образом и компилятор не вызван в одном из стандартных режимов, то _POSIX_C_SOURCE определяется со значением 200809 (200112 с glibc версий от 2.4 до 2.9 или 199506 с glibc версии ниже 2.4).

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

$ cc — D_POSIX_SOURCE — D_POSIX_C_SOURCE=199506 \

— D_BSD_SOURCE — D_SVID_SOURCE prog.c

Дополнительную информацию, уточняющую значения, присваиваемые каждому макросу проверки возможностей, можно найти в заголовочном файле <features.h> и на странице руководства feature_test_macros(7).


_POSIX_C_SOURCE, _XOPEN_SOURCE и POSIX.1/SUS

В POSIX.1-2001/SUSv3 указаны только макросы проверки возможностей _POSIX_C_SOURCE и _XOPEN_SOURCE с требованием, чтобы в соответствующих приложениях они были определены со значениями 200112 и 600. Определение _POSIX_C_SOURCE со значением 200112 обеспечивает соответствие базовой спецификации POSIX.1-2001 (то есть соответствие POSIX, исключая XSI-расширение). Определение _XOPEN_SOURCE со значением 600 обеспечивает соответствие спецификации SUSv3 (то есть соответствие XSI — базовой спецификации плюс XSI-расширению). То же самое относится к POSIX.1-2008/SUSv4 с требованием, чтобы два макроса были определены со значениями 200809 и 700.

В SUSv3 указывается, что установка для _XOPEN_SOURCE значения 600 должна предоставлять все свойства, включаемые, если _POSIX_C_SOURCE присвоено значение 200112. Таким образом, для соответствия SUSv3 (то есть XSI) приложению необходимо определить только _XOPEN_SOURCE. В SUSv4 делается аналогичное требование: установка для _XOPEN_SOURCE значения 700 должна предоставлять все свойства, включаемые, если _POSIX_C_SOURCE присвоено значение 200809.


Макросы проверки возможностей в прототипах функций и в исходном коде примеров

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

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

$ cc — std=c99 — D_XOPEN_SOURCE=600

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


3.6.2. Типы системных данных

При использовании стандартных типов языка C вам предоставляются различные типы данных реализации, например идентификаторы процессов, идентификаторы пользователей и смещения в файлах. Конечно, для объявления переменных, хранящих подобную информацию, можно было бы использовать основные типы языка C, например int и long, но это сокращает возможность портирования между системами UNIX по следующим причинам.

• Размеры этих основных типов от реализации к реализации UNIX отличаются друг от друга (например, long в одной системе может занимать 4 байта, а в другой — 8 байт). Иногда отличия могут прослеживаться даже в разных средах компиляции одной и той же реализации. Кроме того, в разных реализациях для представления одной и той же информации могут использоваться различные типы. Например, в одной системе идентификатор процесса может быть типа int, а в другой — типа long.

• Даже в одной и той же реализации UNIX типы, используемые для представления информации, могут в разных выпусках отличаться друг от друга. Наглядными примерами в Linux могут послужить идентификаторы пользователей и групп. В Linux 2.2 и более ранних версиях эти значения были представлены в 16 разрядах. В Linux 2.4 более поздних версиях они представлены в виде 32-разрядных значений.

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

Каждый из этих типов определен с помощью имеющегося в языке C спецификатора typedef. Например, тип данных pid_t предназначен для представления идентификаторов процессов и в Linux/x86-32 определяется следующим образом:

typedef int pid_t;

У большинства стандартных типов системных данных имена оканчиваются на _t. Многие из них объявлены в заголовочном файле <sys/types.h>, хотя некоторые объявлены в других заголовочных файлах.

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

pid_t mypid;

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


Таблица 3.1. Отдельные типы системных данных

Тип данных — Требование к типу в SUSv3 — Описание

blkcnt_t — Целое число со знаком — Количество блоков файла (см. раздел 15.1)

blksize_t — Целое число со знаком — Размер блока файла (см. раздел 15.1)

cc_t — Целое число без знака — Специальный символ терминала (см. раздел 58.4)

clock_t — Целое число или вещественное число с плавающей точкой — Системное время в тиках часов (см. раздел 10.7)

clockid_t — Арифметический тип — Идентификатор часов для определенных в POSIX.1b функций часов и таймера (см. раздел 23.6)

comp_t — В SUSv3 отсутствует — Сжатые тики часов (см. раздел 28.1)

dev_t — Арифметический тип — Номер устройства, состоящий из старшего и младшего номеров (см. раздел 15.1)

DIR — Требования к типу отсутствуют — Поток каталога (см. раздел 18.8)

fd_set — Структурный тип — Дескриптор файла, установленный для select() (см. подраздел 58.2.1)

fsblkcnt_t — Целое число без знака — Количество блоков в файловой системе (см. раздел 14.11)

fsfilcnt_t — Целое число без знака — Количество файлов (см. раздел 14.11)

gid_t — Целое число — Числовой идентификатор группы (см. раздел 8.3)

id_t — Целое число — Базовый тип для хранения идентификаторов; достаточно большой, по крайней мере для pid_t, uid_t и gid_t

in_addr_t — 32-разрядное целое число без знака — IPv4 адрес (см. раздел 55.4)

in_port_t — 16-разрядное целое число без знака — Номер порта IP (см. раздел 55.4)

ino_t — Целое число без знака — Номер индексного дескриптора файла (см. раздел 15.1)

key_t — Арифметический тип — Ключ IPC в System V

mode_t — Целое число — Тип файла и полномочия доступа к нему (см. раздел 15.1)

mqd_t — Требования к типу отсутствуют, но не должен быть типом массива — Дескриптор очереди сообщений POSIX

msglen_t — Целое число без знака — Количество байтов, разрешенное в очереди сообщений в System V

msgqnum_t — Целое число без знака — Количество сообщений в очереди сообщений в System V

nfds_t — Целое число без знака — Количество дескрипторов файлов для poll() (см. подраздел 59.2.2)

nlink_t — Целое число — Количество жестких ссылок на файл (см. раздел 15.1)

off_t — Целое число со знаком — Смещение в файле или размер файла (см. разделы 4.7 и 15.1)

pid_t — Целое число со знаком — Идентификатор процесса, группы процессов или сессии (см. разделы 6.2, 34.2 и 34.3)

ptrdiff_t — Целое число со знаком — Разница между двумя значениями указателей в виде целого числа со знаком

rlim_t — Целое число без знака — Ограничение ресурса (см. раздел 36.2)

sa_family_t — Целое число без знака — Семейство адресов сокета (см. раздел 52.4)

shmatt_t — Целое число без знака — Количество прикрепленных процессов для совместно используемого сегмента памяти System V

sig_atomic_t — Целое число — Тип данных, который может быть доступен атомарно (см. раздел 21.1.3)

siginfo_t — Структурный тип — Информация об источнике сигнала (см. раздел 21.4)

sigset_t — Целое число или структурный тип — Набор сигналов (см. раздел 20.9)

size_t — Целое число без знака — Размер объекта в байтах

socklen_t — Целочисленный тип, состоящий как минимум из 32 разрядов — Размер адресной структуры сокета в байтах (см. раздел 52.3)

speed_t — Целое число без знака — Скорость строки терминала (см. раздел 58.7)

ssize_t — Целое число со знаком — Количество байтов или (при отрицательном значении) признак ошибки

stack_t — Структурный тип — Описание дополнительного стека сигналов (см. раздел 21.3)

suseconds_t — Целое число со знаком в разрешенном диапазоне [-1, 1 000 000] — Интервал времени в микросекундах (см. раздел 10.1)

tcflag_t — Целое число без знака — Маска флагового разряда режима терминала (см. раздел 58.2)

time_t — Целое число или вещественное число с плавающей точкой — Календарное время в секундах от начала отсчета времени (см. раздел 10.1)

timer_t — Арифметический тип — Идентификатор таймера для функций временных интервалов POSIX.1b (см. раздел 23.6)

uid_t — Целое число — Числовой идентификатор пользователя (см. раздел 8.1)


При рассмотрении типов данных из табл. 3.1 в последующих главах я буду часто говорить, что некий тип «является целочисленным типом (указанным в SUSv3)». Это означает, что SUSv3 требует, чтобы тип был определен в качестве целого числа, но не требует обязательного использования конкретного присущего системе целочисленного типа (например, short, int или long). (Зачастую не будет говориться, какой именно присущий системе тип данных фактически применяется для представления в Linux каждого типа системных данных, поскольку портируемое приложение должно быть написано так, чтобы в нем не ставился вопрос о том, какой тип данных используется.)


Вывод значений типов системных данных

При выводе значений одного из типов системных данных, показанных в табл. 3.1 (например, pid_t и uid_t), нужно проследить, чтобы в вызов функции printf() не была включена зависимость представления данных. Она может возникнуть из-за того, что имеющиеся в языке C правила расширения аргументов приводят к преобразованию значений типа short в int, но оставляют значения типа int и long в неизменном виде. Иными словами, в зависимости от определения типа системных данных вызову printf() передается либо int, либо long. Но, поскольку функция printf() не может определять типы в ходе выполнения программы, вызывающий код должен предоставить эту информацию в явном виде, используя спецификатор формата %d или %ld. Проблема в том, что простое включение в программу одного из этих спецификаторов внутри вызова printf() создает зависимость от реализации. Обычно применяется подход, при котором используется спецификатор %ld, с неизменным приведением соответствующего значения к типу long:

pid_t mypid;

mypid = getpid(); /* Возвращает идентификатор вызывающего процесса */

printf("My PID is %ld\n", (long) mypid);

Из указанного выше подхода следует сделать одно исключение. Поскольку в некоторых средах компиляции тип данных off_t имеет размерность long long, мы приводим off_t-значения к этому типу и в соответствии с описанием из раздела 5.10 используем спецификатор %lld.

В стандарте C99 для printf() определен модификатор длины z, показывающий, что Результат следующего целочисленного преобразования соответствует типу size_t или ssize_t. Следовательно, вместо использования %ld и приведения к этим типам можно указать %zd для ssize_t и аналогично %zu для size_t. Хотя этот спецификатор доступен в glibc, нам нужно избегать его применения, поскольку он доступен не во всех реализациях UNIX.

В стандарте C99 также определен модификатор длины j, который указывает на то, что соответствующий аргумент имеет тип intmax_t (или uintmax_t) — целочисленный тип, гарантированно достаточно большой для представления целого значения любого типа. По сути, использование приведения к типу (intmax_t) и добавление спецификатора %jd должно заменить приведение к типу (long) и задание спецификатора %ld, а также стать лучшим способом вывода числовых значений типов системных данных. Первый подход справляется и со значениями long long, и с любыми расширенными целочисленными типами, такими как int128_t. Но и в данном случае нам следует избегать применения этой методики, поскольку она доступна не во всех реализациях UNIX.


3.6.3. Прочие вопросы, связанные с портированием

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


Инициализация и использование структур

В каждой реализации UNIX указывается диапазон стандартных структур, используемых в различных системных вызовах и библиотечных функциях. Рассмотрим в качестве примера структуру sembuf, которая применяется для представления операции с семафором, выполняемой системным вызовом semop():

struct sembuf {

unsigned short sem_num; /* Номер семафора */

short sem_op; /* Выполняемая операция */

short sem_flg; /* Флаги операции */

};

Хотя в SUSv3 определены такие структуры, как sembuf, важно уяснить следующее.

• Обычно порядок определения полей внутри таких структур не определен.

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

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

struct sembuf s = { 3, -1, SEM_UNDO };

Хотя этот инициализатор будет работать в Linux, он не станет работать в других реализациях, где поля в структуре sembuf определены в ином порядке. Чтобы инициализировать такие структуры портируемым образом, следует воспользоваться явно указанными инструкциями присваивания:

struct sembuf s;

s. sem_num = 3;

s. sem_op = -1;

s. sem_flg = SEM_UNDO;

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

struct sembuf s = {.sem_num = 3, sem_op = -1, sem_flg = SEM_UNDO };

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


Использование макросов, которых может не быть во всех реализациях

В некоторых случаях макрос может быть не определен во всех реализациях UNIX. Например, широкое распространение получил макрос WCOREDUMP() (проверяет, создается ли дочерним процессом файл дампа ядра), но его определение в SUSv3 отсутствует. Следовательно, этот макрос может быть не представлен в некоторых реализациях UNIX. Чтобы для обеспечения портируемости преодолеть подобные обстоятельства, можно воспользоваться директивой препроцессора языка C #ifdef:

#ifdef WCOREDUMP

/* Использовать макрос WCOREDUMP() */

#endif


Отличия в требуемых заголовочных файлах в разных реализациях

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

В некоторых функциях, кратко рассматриваемых в книге, показан конкретный заголовочный файл, сопровождаемый комментарием /* For portability */ (/* Из соображений портируемости */). Это свидетельствует о том, что данный заголовочный файл для Linux или согласно SUSv3 не требуется, но, поскольку некоторым другим (особенно старым) реализациям он может понадобиться, нам приходится включать его в портируемые программы.

Для многих определяемых POSIX.1-1990 функций требуется, чтобы заголовочный файл <sys/types.h> был включен ранее любого другого заголовочного файла, связанного с функцией. Но данное требование стало излишним, поскольку большинство современных реализаций UNIX не требуют от приложений включения этого заголовочного файла. Поэтому из SUSv1 это требование было удалено. И тем не менее при написании портируемых программ будет все же разумнее поставить этот заголовочный файл на первое место. (Но из наших примеров программ этот заголовочный файл исключен, поскольку для Linux он не требуется, и мы можем сократить длину примеров на одну строку.)


3.7. Резюме

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

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

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

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

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

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

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


3.8. Упражнение

3.1. Когда для перезапуска системы используется характерный для Linux системный вызов reboot(), в качестве второго аргумента magic2 необходимо указать одно из магических чисел (например, LINUX_REBOOT_MAGIC2). Какой смысл несут эти числа? (Подсказка: обратите внимание на шестнадцатеричное представление такого числа4.)

3 Подробнее о нем вы можете прочитать по адресу http://microsin.net/programming/arm/lvalue-rvalue.html. — Примеч. пер.

4 Таким образом закодировны дни рождения Торвальдса и его дочерей: https://stackoverflow.com/questions/4808748/magic-numbers-of-the-linux-reboot-system-call.

4. Файловый ввод-вывод: универсальная модель ввода-вывода

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

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

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

В главе 5 рассматриваемые здесь вопросы будут расширены дополнительными сведениями, касающимися файлового ввода-вывода. Еще одна особенность файлового ввода-вывода — буферизация — настолько сложна, что заслуживает отдельной главы. Буферизация ввода-вывода в ядре и с помощью средств библиотеки stdio будет описана в главе 13.


4.1. Общее представление

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

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


Таблица 4.1. Стандартные дескрипторы файлов

Дескриптор файла — Назначение — Имя согласно POSIX — Поток stdio

0 — Стандартный ввод — STDIN_FILENO — stdin

1 — Стандартный вывод — STDOUT_FILENO — stdout

2 — Стандартная ошибка — STDERR_FILENO — stderr


При ссылке на эти дескрипторы файлов в программе можно использовать либо номера (0, 1 или 2), либо, что предпочтительнее, стандартные имена POSIX, определенные в файле <unistd.h>.

Хотя переменные stdin, stdout и stderr изначально ссылаются на стандартные ввод, вывод и ошибку процесса, с помощью библиотечной функции freopen() их можно изменить для ссылки на любой файл. В качестве части своей работы freopen() способен изменить дескриптор файла на основе вновь открытого потока. Иными словами, после вызова freopen() в отношении, к примеру, stdout, нельзя с полной уверенностью предполагать, что относящийся к нему дескриптор файла по-прежнему имеет значение 1.

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

• fd = open(pathname, flags, mode) — открытие файла, идентифицированного по путевому имени — pathname, с возвращением дескриптора файла, который используется для обращения к открытому файлу в последующих вызовах. Если файл не существует, вызов open() может его создать в зависимости от установки битовой маски аргумента флагов — flags. В аргументе флагов также указывается, с какой целью открывается файл: для чтения, для записи или для проведения обеих операций. Аргумент mode (режим), определяет права доступа, которые будут накладываться на файл, если он создается этим вызовом. Если вызов open() не будет использоваться для создания файла, этот аргумент игнорируется и может быть опущен.

• numread = read(fd, buffer, count) — считывание не более указанного в count количества байтов из открытого файла, ссылка на который дана в fd, и сохранение их в буфере buffer. При вызове read() возвращается количество фактически считанных байтов. Если данные не могут быть считаны (то есть встретился конец файла), read() возвращает 0.

• numwritten = write(fd, buffer, count) — запись из буфера байтов, количество которых указано в count, в открытый файл, ссылка на который дана в fd. При вызове write() возвращается количество фактически записанных байтов, которое может быть меньше значения, указанного в count.

• status = close(fd) — вызывается после завершения ввода-вывода с целью высвобождения дескриптора файла fd и связанных с ним ресурсов ядра.

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

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

$ ./copy oldfile newfile


Листинг 4.1. Использование системных вызовов ввода-вывода

fileio/copy.c

#include <sys/stat.h>

#include <fcntl.h>

#include "tlpi_hdr.h"


#ifndef BUF_SIZE /* Позволяет "cc — D" перекрыть определение */

#define BUF_SIZE 1024

#endif


int

main(int argc, char *argv[])

{

int inputFd, outputFd, openFlags;

mode_t filePerms;

ssize_t numRead;

char buf[BUF_SIZE];


if (argc!= 3 || strcmp(argv[1], "-help") == 0)

usageErr("%s old-file new-file\n", argv[0]);

/* Открытие файлов ввода и вывода */

inputFd = open(argv[1], O_RDONLY);

if (inputFd == -1)

errExit("opening file %s", argv[1]);

openFlags = O_CREAT | O_WRONLY | O_TRUNC;

filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |

S_IROTH | S_IWOTH; /* rw-rw-rw- */

outputFd = open(argv[2], openFlags, filePerms);

if (outputFd == -1)

errExit("opening file %s", argv[2]);


/* Перемещение данных до достижения конца файла ввода или возникновения ошибки */

while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)

if (write(outputFd, buf, numRead)!= numRead)

fatal("couldn't write whole buffer");

if (numRead == -1)

errExit("read");

if (close(inputFd) == -1)

errExit("close input");

if (close(outputFd) == -1)

errExit("close output");


exit(EXIT_SUCCESS);

}

fileio/copy.c


4.2. Универсальность ввода-вывода

Одна из отличительных особенностей модели ввода-вывода UNIX состоит в универсальности ввода-вывода. Это означает, что одни и те же четыре системных вызова — open(), read(), write() и close() — применяются для выполнения ввода-вывода во всех типах файлов, включая устройства, например терминалы. Следовательно, если программа написана с использованием лишь этих системных вызовов, она будет работать с любым типом файла. Например, следующие примеры показывают вполне допустимое использование программы, чей код приведен в листинге 4.1:

$ ./copy test test.old Копирование обычного файла

$ ./copy a.txt /dev/tty Копирование обычного файла в этот терминал

$ ./copy /dev/tty b.txt Копирование ввода с этого терминала в обычный файл

$ ./copy /dev/pts/16 /dev/tty Копирование ввода с другого терминала

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


4.3. Открытие файла: open()

Системный вызов open() либо открывает существующий файл, либо создает и открывает новый файл.

#include <sys/stat.h>

#include <fcntl.h>


int open(const char *pathname, int flags… /* mode_t mode */);

Возвращает дескриптор при успешном завершении или –1 при ошибке

Чтобы файл открылся, он должен пройти идентификацию по аргументу pathname. Если в этом аргументе находится символьная ссылка, она разыменовывается. В случае успеха open() возвращает дескриптор файла, который используется для ссылки на файл в последующих системных вызовах. В случае ошибки open() возвращает –1, а для errno устанавливается соответствующее значение.

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

В ранних версиях UNIX вместо имен, приведенных в табл. 4.2, использовались числа 0, 1 и 2. В более современных реализациях UNIX эти константы определяются с указанными в таблице значениями. Из нее видно, что O_RWDW (10 в двоичном представлении) не совпадает с результатом операции O_RDONLY | O_WRONLY (0 | 1 = 1); последняя комбинация прав доступа является логической ошибкой.

Когда open() применяется для создания нового файла, аргумент битовой маски режима (mode) указывает на права доступа, которые должны быть присвоены файлу. (Используемый тип данных mode_t является целочисленным типом, определенным в SUSv3.) Если при вызове open() не указывается флаг O_CREAT, то аргумент mode может быть опущен.


Таблица 4.2. Режимы доступа к файлам

Режим доступа

Описание

O_RDONLY

Открытие файла только для чтения

O_WRONLY

Открытие файла только для записи

O_RDWR

Открытие файла как для чтения, так и для записи

Подробное описание прав доступа дается в разделе 15.4. Позже будет показано, что права доступа, фактически присваиваемые новому файлу, зависят не только от аргумента mode, но и от значения umask процесса (см. подраздел 15.4.6) и от (дополнительно имеющегося) списка контроля доступа по умолчанию (access control list) (см. раздел 17.6) родительского каталога. А пока просто отметим для себя, что аргумент mode может быть указан в виде числа (обычно восьмеричного) или, что более предпочтительно, путем применения операции логического ИЛИ (|) к нескольким константам битовой маски, перечисленным в табл. 15.4.

В листинге 4.2 показаны примеры использования open(). В некоторых из них указываются дополнительные биты флагов, которые вскоре будут рассмотрены.


Листинг 4.2. Примеры использования open()

/* Открытие существующего файла для чтения */

fd = open("startup", O_RDONLY);


if (fd == -1)

errExit("open");

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

байтов; предоставление владельцу исключительных прав доступа на чтение и запись */


fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);

if (fd == -1)

errExit("open");

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

должны всегда добавляться в конец файла */

fd = open("w.log", O_WRONLY | O_CREAT | O_APPEND,

S_IRUSR | S_IWUSR);

if (fd == -1)

errExit("open");


Номер дескриптора файла, возвращаемый системным вызовом open()

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

if (close(STDIN_FILENO) == -1) /* Закрытие нулевого файлового дескриптора */

errExit("close");

fd = open(pathname, O_RDONLY);

if (fd == -1)

errExit("open");

Поскольку дескриптор файла 0 не используется, open() гарантирует открытие файла с этим дескриптором. В разделе 5.5 показывается применение для получения аналогичного результата вызовов dup2() и fcntl(), но с более гибким управлением дескриптором файла. В этом разделе также приводится пример того, как можно извлечь пользу от управления файловым дескриптором, с которым открывается файл.


4.3.1. Аргумент flags системного вызова open()

В некоторых примерах вызова open(), показанных в листинге 4.2, во флаги, кроме режима доступа к файлу, включены дополнительные биты (O_CREAT, O_TRUNC и O_APPEND). Рассмотрим аргумент flags более подробно. В табл. 4.3 приведен полный набор констант, любая комбинация которых с помощью побитового ИЛИ (|), может быть передана в аргументе flags. В последнем столбце показано, какие из этих констант были включены в стандарт SUSv3 или SUSv4.


Таблица 4.3. Значения для аргументов флагов системного вызова open()

Флаг — Назначение — SUS?

O_RDONLY — Открытие только для чтения — v3

O_WRONLY — Открытие только для записи — v3

O_RDWR — Открытие для чтения и записи — v3

O_CLOEXEC — Установка флага закрытия при выполнении (close-on-exec) (начиная с версии Linux 2.6.23) — v4

O_CREAT — Создание файла, если он еще не существует — v3

O_DIRECTORY — Отказ, если аргумент pathname указывает не на каталог — v4

O_EXCL — С флагом O_CREAT: исключительное создание файла — v3

O_LARGEFILE — Используется в 32-разрядных системах для открытия больших файлов -

O_NOCTTY — Pathname запрещено становиться управляющим терминалом данного процесса — v3

O_NOFOLLOW — Запрет на разыменование символьных ссылок — v4

O_TRUNC — Усечение существующего файла до нулевой длины — v3

O_APPEND — Записи добавляются исключительно в конец файла — v3

O_ASYNC — Генерация сигнала, когда возможен ввод/вывод —

O_DIRECT — Операции ввода-вывода осуществляются без использования кэша —

O_DSYNC — Синхронизированный ввод-вывод с обеспечением целостности данных (начиная с версии Linux 2.6.33) — v3

O_NOATIME — Запрет на обновление времени последнего доступа к файлу при чтении с помощью системного вызова read() (начиная с версии Linux 2.6.8) -

O_NONBLOCK — Открытие в неблокируемом режиме — v3

O_SYNC — Ведение записи в файл в синхронном режиме — v3


Константы в табл. 4.3 разделяются на следующие группы.

• Флаги режима доступа к файлу. Это рассмотренные ранее флаги O_RDONLY, O_WRONLY и O_RDWR. Их можно извлечь с помощью операции F_GETFL функции fcntl() (см. раздел 5.3).

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

Флаги состояния открытия файла. Это все остальные флаги, показанные в табл. 4.3. Они могут быть извлечены и изменены с помощью операций F_GETFL и F_SETFL функции fcntl() (см. раздел 5.3). Эти флаги иногда называют просто флагами состояния файла.

Начиная с версии ядра 2.6.22, для получения информации о дескрипторах файлов любого имеющегося в системе процесса могут быть прочитаны файлы в каталоге /proc/PID/fdinfo, которые есть только в Linux. В этом каталоге находится по одному файлу для каждого дескриптора открытого процессом файла, с именем, совпадающим с номером дескриптора. В поле pos этого файла показано текущее смещение в файле (см. раздел 4.7). В поле flags находится восьмеричное число, показывающее флаги режима доступа к файлу и флаги состояния открытого файла. (Чтобы декодировать эти числа, нужно посмотреть на числовые значения флагов в заголовочных файлах библиотеки языка C.)

Рассмотрим константы флагов подробнее.

• O_APPEND — записи добавляются исключительно в конец файла. Значение этого флага рассматривается в разделе 5.1.

• O_ASYNC — генерирование сигнала при появлении возможности ввода-вывода с использованием файлового дескриптора, возвращенного системным вызовом open(). Это свойство называется вводом-выводом под управлением сигналов. Оно доступно только для файлов определенного типа, таких как терминалы, FIFO-устройства и сокеты. (Флаг O_ASYNC не определен в SUSv3, но в большинстве реализаций UNIX он или его синоним FASYNC присутствует.) В Linux указание флага O_ASYNC при вызове open() не имеет никакого эффекта. Чтобы включить ввод/вывод с сигнальным управлением, нужно установить этот флаг, указывая в fcntl() операцию F_SETFL (см. раздел 5.3). (Некоторые другие реализации UNIX ведут себя аналогичным образом.) Дополнительные сведения о флаге O_ASYNC можно найти в разделе 59.3.

• O_CLOEXEC (с выходом Linux 2.6.23) — установка флага закрытия при выполнении — флага close-on-exec (FD_CLOEXEC) для нового дескриптора файла. Флаг FD_CLOEXEC рассматривается в разделе 27.4. Использование флага O_CLOEXEC позволяет программе не выполнять дополнительные операции F_GETFD и F_SETFD при вызове fcntl() для установки флага close-on-exec. Кроме того, в многопоточных программах необходимо избегать состояния гонки, возможное при использовании данной технологии. Например, такое состояние может возникать, когда один поток открывает дескриптор файла, а затем пытается пометить его флагом close-on-exec, и в то же самое время другой поток выполняет системный вызов fork(), а затем exec() из какой-нибудь другой программы. (Предположим, что второй поток справляется и с fork(), и с exec() в период между тем, как первый поток открывает файловый дескриптор и использует fcntl() для установки флага close-on-exec.) Такое состязание может привести к тому, что открытые дескрипторы файлов могут непреднамеренно быть переданы небезопасным программам. (Состояние гонки подробнее рассматривается в разделе 5.1.)

• O_CREAT — создание нового, пустого файла, если такого файла еще не существует. Этот флаг срабатывает, даже если файл открывается только для чтения. Если указывается O_CREAT, то при вызове open() нужно также обязательно предоставлять аргумент mode. В противном случае права доступа к новому файлу будут установлены по какому-либо произвольному значению, взятому из стека.

• O_DIRECT — разрешение файловому вводу-выводу обходить буферный кэш. Это свойство рассматривается в разделе 13.6. Чтобы сделать определение этой константы доступным из <fcntl.h>, должен быть задан макрос проверки возможностей _GNU_SOURCE.

• O_DIRECTORY — возвращение ошибки (в этом случае errno присваивается значение ENOTDIR), если путевое имя не является каталогом. Этот флаг представляет собой расширение, разработанное главным образом для реализации opendir() (см. раздел 18.8). Чтобы сделать определение этой константы доступным из <fcntl.h>, должен быть задан макрос проверки возможностей _GNU_SOURCE.

• O_DSYNC (с выходом Linux 2.6.33) — выполнение записи в файл в соответствии с требованиями соблюдения целостности данных при синхронизированном вводе-выводе. Обратите внимание на буферизацию ввода-вывода на уровне ядра, рассматриваемую в разделе 13.3.

• O_EXCL — используется в сочетании с флагом O_CREAT как указание, что файл, если он уже существует, не должен быть открыт. Вместо этого системный вызов open() не выполняется, а errno присваивается значение EEXIST. Иными словами, флаг позволяет вызывающему коду убедиться в том, что это и есть процесс, создающий файл. Проверка существования и создание файла выполняются в атомарном режиме. Понятие атомарности рассматривается в разделе 5.1. Когда в качестве флагов указаны и O_CREAT, и O_EXCL, системный вызов open() не выполняется (с ошибкой EEXIST), если путевое имя является символьной ссылкой. Такое поведение в SUSv3 требуется, чтобы привилегированные приложения могли создавать файл в определенном месте и при этом исключалась возможность создания файла в другом месте с использованием символьной ссылки (например, в системном каталоге), которая негативно скажется на безопасности.

• O_LARGEFILE — открытие файла в режиме поддержки больших файлов. Этот флаг применяется в 32-разрядных системах для работы с большими файлами. Хотя флаг O_LARGEFILE в SUSv3 не указан, его можно найти в некоторых других реализациях UNIX. В 64-разрядных реализациях Linux, таких как Alpha и IA-64, этот флаг работать не будет. Дополнительные сведения о нем даются в разделе 5.10.

• O_NOATIME (с выходом Linux 2.6.8) — отказ от обновления времени последнего обращения к файлу (поле st_atime рассматривается в разделе 15.1) при чтении из файла. Чтобы можно было воспользоваться этим флагом, действующий идентификатор пользователя вызывающего процесса должен соответствовать владельцу файла или же процесс должен быть привилегированным (CAP_FOWNER). В противном случае системный вызов open() не будет выполнен и будет выдана ошибка EPERM. (В действительности, как указывается в разделе 9.5, для непривилегированного процесса речь идет о пользовательском идентификаторе файловой системы, а не о его действующем ID пользователя. Именно он должен совпадать с идентификатором пользователя файла при открытии этого файла с флагом O_NOATIME.) Флаг относится к нестандартным расширениям Linux. Для предоставления его определения из <fcntl.h> следует задать макрос проверки возможностей _GNU_SOURCE. Флаг O_NOATIME предназначен для использования программами индексации и создания резервных копий. Его применение может существенно сократить объем активного использования диска, поскольку не потребуются многочисленные перемещения вперед и назад по диску для чтения содержимого файла, а также обновления времени последнего обращения к файлу в индексном дескрипторе (см. раздел 14.4). Функциональные возможности, похожие на обеспечиваемые флагом O_NOATIME, доступны при использовании флагов MS_NOATIME и FS_NOATIME_FL (см. раздел 15.5) во время системного вызова mount() (см. подраздел 14.8.1).

• O_NOCTTY — предотвращение превращения открываемого файла в управляющий терминал, если он является терминальным устройством. Управляющие терминалы рассматриваются в разделе 34.4. Если открываемый файл не является терминалом, флаг не работает.

• O_NOFOLLOW — обычно системный вызов open() разыменовывает символьную ссылку. Но, если задан флаг O_NOFOLLOW и аргумент pathname является символьной ссылкой, вызов open() не выполняется (в errno заносится значение ELOOP). Этот флаг особенно пригодится в привилегированных программах, чтобы обеспечить отказ от разыменования символьной ссылки при системном вызове open(). Для предоставления определения этого флага из <fcntl.h> следует добавить макрос проверки возможностей _GNU_SOURCE.

• O_NONBLOCK — открытие файла в неблокируемом режиме (см. раздел 5.9).

• O_SYNC — открытие файла для синхронизированного ввода-вывода. Обратите внимание на буферизацию ввода-вывода на уровне ядра, рассматриваемую в разделе 13.3.

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


4.3.2. Ошибки, возвращаемые из системного вызова open()

В случае возникновения ошибки при попытке открытия файла системный вызов open() возвращает –1, а в errno идентифицируется причина ошибки. Далее перечислены возможные ошибки, которые могут произойти (вдобавок к тем, что уже были упомянуты при описании только что рассмотренного аргумента flags).

• EACCES — права доступа к файлу не позволяют вызывающему процессу открыть файл в режиме, указанном флагами. Из-за прав доступа к каталогу доступ к файлу невозможен или файл не существует и не может быть создан.

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

• EMFILE — достигнуто ограничение ресурса процесса на количество файловых дескрипторов (RLIMIT_NOFILE, рассматривается в разделе 36.3).

• ENFILE — достигнуто ограничение на количество открытых файлов, накладываемое на всю систему.

• ENOENT — заданный файл не существует, и ключ O_CREAT не указан; или O_CREAT был указан, и один из каталогов в путевом имени не существует или является символьной ссылкой, ведущей на несуществующее путевое имя (битой ссылкой).

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

• ETXTBSY — заданный файл является исполняемым (программой), и в данный момент выполняется. Изменение исполняемого файла, связанного с выполняемой программой (то есть его открытие для записи), запрещено. (Чтобы изменить исполняемый файл, сначала следует завершить программы.)

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


4.3.3. Системный вызов creat()

В ранних реализациях UNIX у open() было только два аргумента, и этот вызов нельзя было использовать для создания нового файла. Вместо него для создания и открытия нового файла использовался системный вызов creat().

#include <fcntl.h>


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

Возвращает дескриптор файла или –1 при ошибке

Системный вызов creat() создает и открывает новый файл с заданным путевым именем или, если файл уже существует, открывает файл и усекает его до нулевой длины. В качестве результата своей работы creat() возвращает дескриптор файла, который может быть использован в последующих системных вызовах. Вызов creat() эквивалентен такому вызову open():

fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);

Поскольку аргумент flags системного вызова open() предоставляет больше контроля над тем, как открывается файл (например, вместо O_WRONLY можно указать O_RDWR), системный вызов creat() теперь считается устаревшим, хотя в старых программах он еще встречается.


4.4. Чтение из файла: read()

Системный вызов read() позволяет считывать данные из открытого файла, на который ссылается дескриптор fd.

#include <unistd.h>


ssize_t read(int fd, void *buffer, size_t count);

Возвращает количество считанных байтов, 0 при EOF или –1 при ошибке

Аргумент count определяет максимальное количество считываемых байтов (тип данных size_t — беззнаковый целочисленный). Аргумент buffer предоставляет адрес буфера памяти, в который должны быть помещены входные данные. Этот буфер должен иметь длину в байтах не менее той, что задана в аргументе count.

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

При успешном вызове read() возвращается количество фактически считанных байтов или 0, если встретился символ конца файла. При ошибке обычно возвращается –1. Тип данных ssize_t относится к целочисленному типу со знаком. Этот тип используется для хранения количества байтов или значения –1, которое служит признаком ошибки.

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

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

При использовании read() для ввода последовательности символов из, скажем, терминала, можно предполагать, что сработает следующий код:

#define MAX_READ 20

char buffer[MAX_READ];


if (read(STDIN_FILENO, buffer, MAX_READ) == -1)

errExit("read");

printf("The input data was: %s\n", buffer);

Этот фрагмент кода выведет весьма странные данные, поскольку в них, скорее всего, будут включены символы, дополняющие фактически введенную строку. Дело в том, что вызов read() не добавляет завершающий нулевой байт в конце строки, которая задается для вывода функции printf(). Нетрудно догадаться, что именно так и должно быть, поскольку read() может использоваться для чтения любой последовательности байтов из файла. В некоторых случаях входные данные могут быть текстом, но бывает, что это двоичные целые числа или структуры языка C в двоичном виде. Невозможно «объяснить» вызову read() разницу между ними, поэтому он не в состоянии выполнять соглашение языка C о завершении строки символов нулевым байтом. Если в конце буфера входных данных требуется наличие завершающего нулевого байта, его нужно вставлять явным образом:

char buffer[MAX_READ + 1];

ssize_t numRead;


numRead = read(STDIN_FILENO, buffer, MAX_READ);

if (numRead == -1)

errExit("read");

buffer[numRead] = '\0';

printf("The input data was: %s\n", buffer);

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


4.5. Запись в файл: write()

Системный вызов write() записывает данные в открытый файл.

#include <unistd.h>


ssize_t write(int fd, const void *buffer, size_t count);

Возвращает количество записанных байтов или –1 при ошибке

Аргументы для write() аналогичны тем, что использовались для read(): buffer представляет собой адрес записываемых данных, count является количеством записываемых из буфера данных, а fd содержит дескриптор файла, который ссылается на тот файл, куда будут записываться данные.

В случае успеха вызов write() возвращает количество фактически записанных данных, которое может быть меньше значения аргумента count. Для дискового файла возможными причинами такой частичной записи может оказаться переполнение диска или достижение ограничения ресурса процесса на размеры файла. (Речь идет об ограничении RLIMIT_FSIZE, которое рассматривается в разделе 36.3.)

При выполнении ввода-вывода в отношении дискового файла успешный выход из write() не гарантирует перемещения данных на диск, поскольку ядро занимается буферизацией дискового ввода-вывода, чтобы сократить объем работы с диском и ускорить выполнение системного вызова write(). Более подробно этот вопрос рассматривается в главе 13.


4.6. Закрытие файла: close()

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

#include <unistd.h>


int close(int fd);

Возвращает 0 при успешном завершении или –1 при ошибке

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

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

if (close(fd) == -1)

errExit("close");

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

Сетевая файловая система — NFS (Network File System) — предоставляет пример такой специфичной для нее ошибки. Когда в NFS происходит сбой завершения транзакции, означающий, что данные не достигли удаленного диска, эта ошибка доходит до приложения в виде сбоя системного вызова close().


4.7. Изменение файлового смещения: lseek()

Для каждого открытого файла в ядре записывается файловое смещение, которое иногда также называют смещением чтения-записи или указателем. Оно обозначает место в файле, откуда будет стартовать работа следующего системного вызова read() или write(). Файловое смещение выражается в виде обычной байтовой позиции относительно начала файла. Первый байт файла расположен со смещением 0.

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

Системный вызов lseek() устанавливает файловое смещение открытого файла, на который указывает дескриптор fd, в соответствии со значениями, заданными в аргументах offset и whence.

#include <unistd.h>


off_t lseek(int fd, off_t offset, int whence);

Возвращает новое файловое смещение при успешном завершении или –1 при ошибке

Аргумент offset определяет значение смещения в байтах. (Тип данных off_t — целочисленный тип со знаком, определенный в SUSv3.) Аргшумент whence указывает на отправную точку, от которой отсчитывается смещение, и может иметь следующие значения:

• SEEK_SET — файловое смещение устанавливается в байтах на расстоянии offset от начала файла;

• SEEK_CUR — смещение устанавливается в байтах на расстоянии offset относительно текущего файлового смещения;

• SEEK_END — файловое смещение устанавливается на размер файла плюс offset. Иными словами, offset рассчитывается относительно следующего байта после последнего байта файла.

Порядок интерпретации аргумента whence показан на рис. 4.1.

В ранних реализациях UNIX вместо констант SEEK_*, перечисленных выше, использовались целые числа 0, 1 и 2. В старых версиях BSD для этих значений применялись другие имена: L_SET, L_INCR и L_XTND.



Рис. 4.1. Интерпретация аргумента whence системного вызова lseek()


Если аргумент whence содержит значение SEEK_CUR или SEEK_END, то у аргумента offset может быть положительное или отрицательное значение. Для SEEK_SET значение offset должно быть неотрицательным.

При успешном выполнении lseek() возвращается значение нового файлового смещения. Следующий вызов извлекает текущее расположение файлового смещения, не изменяя его значения:

curr = lseek(fd, 0, SEEK_CUR);

В некоторых реализациях UNIX (но не в Linux) имеется нестандартная функция tell(fd), которая служит той же цели, что и описанный системный вызов lseek().

Рассмотрим некоторые другие примеры вызовов lseek(), а также комментарии, объясняющие, куда передвигается файловое смещение:

lseek(fd, 0, SEEK_SET); /* Начало файла */

lseek(fd, 0, SEEK_END); /* Следующий байт после конца файла */

lseek(fd, — 1, SEEK_END); /* Последний байт файла */

lseek(fd, — 10, SEEK_CUR); /* Десять байтов до текущего размещения */

lseek(fd, 10000, SEEK_END); /* 10 000 и 1 байт после

последнего байта файла */

Вызов lseek() просто устанавливает значение для записи ядра, содержащей файловое смещение и связанной с дескриптором файла. Никакого физического доступа к устройству при этом не происходит.

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

Не ко всем типам файлов можно применять системный вызов lseek(). Запрещено применение lseek() к конвейеру, FIFO-устройству, сокету или терминалу — вызов аварийно завершится с установленным для errno значением ESPIPE. С другой стороны, lseek() можно применять к тем устройствам, в отношении которых есть смысл это делать, например, при наличии возможности установки на конкретное место на дисковом или ленточном устройстве.

Буква l в названии lseek() появилась из-за того, что как для аргумента offset, так и для возвращаемого значения первоначально определялся тип long. В ранних реализациях UNIX предоставлялся системный вызов seek(), в котором для этих значений определялся тип int.


Файловые дыры

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

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

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

Утверждение о том, что файловые дыры не потребляют дисковое пространство, требует уточнения. На большинстве файловых систем файловое пространство выделяется поблочно (см. раздел 14.3). Размер блока зависит от типа файловой системы, но обычно составляет 1024, 2048 или 4096 байт. Если край дыры попадает в блок, а не на границу блока, тогда для хранения байтов в другой части блока выделяется весь блок, и та часть, которая относится к дыре, заполняется нулевыми байтами.

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

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

В SUSv3 определена функция posix_fallocate(fd, offset, len). Она гарантирует выделение дискового пространства для байтового диапазона, указанного аргументами offset и len для дискового файла, ссылка на который дается в дескрипторе fd. Это позволяет приложению получить гарантию, что при последующем вызове write() в отношении данного файла не будет сбоя, связанного с исчерпанием дискового пространства (который в противном случае может произойти при заполнении дыры в файле или потреблении дискового пространства каким-нибудь другим приложением). Исторически, реализация этой функции в glibc достигает нужного результата, записывая в каждый блок указанного диапазона нули. Начиная с версии 2.6.23, в Linux предоставляется системный вызов fallocate(). Он предлагает более эффективный способ обеспечения выделения необходимого пространства, и реализация posix_fallocate() в glibc использует этот системный вызов при его доступности.

В разделе 14.4 описывается способ представления дыр в файле, а в разделе 15.1 рассматривается системный вызов stat(), который способен сообщить о текущем размере файла, а также о количестве блоков, фактически выделенных файлу.


Пример программы

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

• soffset — установка байтового смещения offset с начала файла;

• rlength — чтение length байтов из файла, начиная с текущего файлового смещения, и вывод их в текстовой форме;

• Rlength — чтение length байтов из файла, начиная с текущего файлового смещения и вывод их в виде шестнадцатеричных чисел;

• wstr — запись строки символов, указанной в str, начиная с позиции текущего файлового смещения.


Листинг 4.3. Демонстрация работы read(), write() и lseek()

fileio/seek_io.c

#include <sys/stat.h>

#include <fcntl.h>

#include <ctype.h>

#include "tlpi_hdr.h"


int

main(int argc, char *argv[])

{

size_t len;

off_t offset;

int fd, ap, j;

char *buf;

ssize_t numRead, numWritten;


if (argc < 3 || strcmp(argv[1], "-help") == 0)

usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}…\n",

argv[0]);


fd = open(argv[1], O_RDWR | O_CREAT,

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |

S_IROTH | S_IWOTH); /* rw-rw-rw- */

if (fd == -1)

errExit("open");


for (ap = 2; ap < argc; ap++) {

switch (argv[ap][0]) {

case 'r': /* Вывод байтов с позиции текущего смещения в виде текста */

case 'R': /* Вывод байтов с позиции текущего смещения в виде hex-чисел */

len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);

buf = malloc(len);

if (buf == NULL)

errExit("malloc");


numRead = read(fd, buf, len);

if (numRead == -1)

errExit("read");


if (numRead == 0) {

printf("%s: end-of-file\n", argv[ap]);

} else {

printf("%s: ", argv[ap]);

for (j = 0; j < numRead; j++) {

if (argv[ap][0] == 'r')

printf("%c", isprint((unsigned char) buf[j])?

buf[j]: '?');

else

printf("%02x", (unsigned int) buf[j]);

}

printf("\n");

}


free(buf);

break;


case 'w': /* Запись строки, начиная с позиции текущего смещения */

numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));

if (numWritten == -1)

errExit("write");

printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten);

break;

case 's': /* Изменение файлового смещения */

offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);

if (lseek(fd, offset, SEEK_SET) == -1)

errExit("lseek");

printf("%s: seek succeeded\n", argv[ap]);

break;


default:

cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]);

}

}


exit(EXIT_SUCCESS);

}

fileio/seek_io.c

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

$ touch tfile Создание нового, пустого файла5

$ ./seek_io tfile s100000 wabc Установка смещения 100000, запись “abc”

s100000: seek succeeded

wabc: wrote 3 bytes

$ ls — l tfile Проверка размера файла

— rw-r-r- 1 mtk users 100003 Feb 10 10:35 tfile

$ ./seek_io tfile s10000 R5 Установка смещения 10000, чтение пяти байт из дыры

s10000: seek succeeded

R5: 00 00 00 00 00 В байтах дыры содержится 0


4.8. Операции, не вписывающиеся в модель универсального ввода-вывода: ioctl()

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

#include <sys/ioctl.h>


int ioctl(int fd, int request… /* argp */);

Возвращаемое при успешном завершении значение зависит от request или при ошибке равно –1

Аргумент fd содержит дескриптор открываемого файла, представленного устройством или файлом, в отношении которого выполняется управляющая операция (указана в аргументе request). Как показывает стандартная для языка C запись в виде многоточия (…), третий аргумент для ioctl(), обозначенный как argp, может быть любого типа. Аргумент request позволяет ioctl() определить, какого типа значение следует ожидать в argp. Обычно argp представляет собой указатель либо на целое число, либо на структуру. В некоторых случаях этот аргумент не применяется.

Использование ioctl() будет показано в следующих главах (к примеру, в разделе 15.5).

Единственная спецификация, имеющаяся в SUSv3 для ioctl(), регламентирует операции по управлению STREAMS-устройствами. (Среда STREAMS относится к особенностям System V, не поддерживаемым основной ветвью ядра Linux, хотя было разработано несколько реализаций в виде дополнений.) Ни одна из других рассматриваемых в книге операций ioctl() в SUSv3 не регламентирована. Но вызов ioctl() был частью системы UNIX с самых ранних версий, вследствие чего несколько операций ioctl() предоставляются во многих других реализациях UNIX. По мере рассмотрения каждой операции ioctl() будут обсуждаться и вопросы портируемости.


4.9. Резюме

Чтобы выполнить ввод/вывод в отношении обычного файла, сначала нужно получить его дескриптор, воспользовавшись системным вызовом open(). Затем ввод/вывод выполняется с помощью системных вызовов read() и write(). После завершения всех операций ввода-вывода следует высвободить дескриптор файла и связанные с ним ресурсы, воспользовавшись системным вызовом close(). Эти системные вызовы могут применяться для выполнения ввода-вывода в отношении всех типов файлов.

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

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

Системный вызов ioctl() предлагает для устройства и файла разнообразные операции, которые не вписываются в стандартную модель файлового ввода-вывода.


4.10. Упражнения

4.1. Команда tee считывает свой стандартный ввод, пока ей не встретится символ конца файла, записывает копию своего ввода на стандартное устройство вывода и в файл, указанный в аргументе ее командной строки. (Пример использования этой команды будет показан при рассмотрении FIFO-устройств в разделе 44.7.) Реализуйте tee, используя системные вызовы ввода-вывода. По умолчанию tee перезаписывает любой существующий файл с заданным именем. Укажите ключ командной строки — a (tee — a file), который заставит tee добавлять текст к концу уже существующего файла.

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

5 Только если файла с таким именем еще не было в текущем каталоге. Иначе эта команда лишь обновит время последнего обращения к файлу. — Примеч. пер.

5. Файловый ввод-вывод: дополнительные сведения

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

Будет представлен еще один многоцелевой системный вызов, имеющий отношение к файлам, — fcntl(). Мы рассмотрим один из примеров его использования: извлечение и установку флагов состояния открытого файла.

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

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

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

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


5.1. Атомарность и состояние гонки

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

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

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

Мы вернемся к теме состояния гонки, когда приступим к рассмотрению системного вызова sigsuspend() в разделе 22.9 и системного вызова fork() в разделе 24.4.


Эксклюзивное создание файла

В подразделе 4.3.1 отмечалось, что указание флага O_EXCL в сочетании с флагом O_CREAT заставляет open() возвращать ошибку, если файл уже существует. Тем самым процессу гарантируется, что именно он является создателем файла. Проводимая заранее проверка существования файла и создание файла выполняются атомарно. Чтобы понять, насколько это важно, рассмотрим код, показанный в листинге 5.1. Мы могли бы им воспользоваться при отсутствии флага O_EXCL. (В этом коде выводится идентификатор процесса, возвращаемый системным вызовом getpid(), позволяющий отличить данные на выходе двух различных запусков этой программы.)


Листинг 5.1. Код, не подходящий для эксклюзивного открытия файла

Из файла fileio/bad_exclusive_open.c

fd = open(argv[1], O_WRONLY); /* Открытие 1: проверка существования файла */

if (fd!= -1) { /* Открытие прошло успешно */

printf("[PID %ld] File \"%s\" already exists\n",

(long) getpid(), argv[1]);

close(fd);

} else {

if (errno!= ENOENT) { /* Сбой по неожиданной причине */

errExit("open");

} else {

/* ОТРЕЗОК ВРЕМЕНИ НА СБОЙ */

fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);

if (fd == -1)

errExit("open");

printf("[PID %ld] Created file \"%s\" exclusively\n",

(long) getpid(), argv[1]); /* МОЖЕТ БЫТЬ ЛОЖЬЮ! */

}

}

Из файла fileio/bad_exclusive_open.c

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

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

Чтобы показать несомненную проблемность кода, можно заменить закомментированную строку ОТРЕЗОК ВРЕМЕНИ НА СБОЙ в листинге 5.1 фрагментом кода, создающим искусственную задержку между проверкой существования файла и созданием файла:



Рис. 5.1. Неудачная попытка эксклюзивного создания файла


printf("[PID %ld] File \"%s\" doesn't exist yet\n", (long) getpid(), argv[1]);

if (argc > 2) { /* Задержка между проверкой и созданием */

sleep(5); /* Приостановка выполнения на 5 секунд */

printf("[PID %ld] Done sleeping\n", (long) getpid());

}

Библиотечная функция sleep() приостанавливает выполнение процесса на указанное количество секунд. Эта функция рассматривается в разделе 23.4.

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

$ ./bad_exclusive_open tfile sleep &

[PID 3317] File "tfile" doesn't exist yet

[1] 3317

$ ./bad_exclusive_open tfile

[PID 3318] File "tfile" doesn't exist yet

[PID 3318] Created file "tfile" exclusively

$ [PID 3317] Done sleeping

[PID 3317] Created file "tfile" exclusively Ложь

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

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


Добавление данных к файлу

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

if (lseek(fd, 0, SEEK_END) == -1)

errExit("lseek");

if (write(fd, buf, len)!= len)

fatal("Partial/failed write");

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

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

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


5.2. Операции управления файлом: fcntl()

Системный вызов fcntl() может выполнять операции управления, используя дескриптор открытого файла.

#include <fcntl.h>


int fcntl(int fd, int cmd, …);

Значение, возвращаемое при успешном завершении, зависит от значения cmd или равно –1 при сбое

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

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


5.3. Флаги состояния открытого файла

Один из примеров использования fcntl() — извлечение или изменение флагов режима доступа и состояния открытого файла. (Это значения, установленные аргументом flags, указанным в вызове open().) Чтобы извлечь эти установки, для cmd указывается значение F_GETFL:

int flags, accessMode;


flags = fcntl(fd, F_GETFL); /* Третий аргумент не требуется */

if (flags == -1)

errExit("fcntl");

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

if (flags & O_SYNC)

printf("записи синхронизированы \n");

В SUSv3 требуется, чтобы открытому файлу соответствовали лишь те флаги, которые были указаны при системном вызове open() или последующих операциях F_SETFL вызова fcntl(). В Linux есть единственное отклонение от этого требования: если приложение было скомпилировано с использованием одного из подходов, рассматриваемых в разделе 5.10 для открытия больших файлов, то среди флагов, извлекаемых операцией F_GETFL всегда будет установлен O_LARGEFILE.

Проверка режима доступа к файлу происходит немного сложнее, поскольку константы O_RDONLY (0), O_WRONLY (1) и O_RDWR (2) не соответствуют отдельным разрядам флагов состояния открытого файла. По этой причине на значение флагов накладывается маска с помощью константы O_ACCMODE, а затем проводится проверка на равенство одной из констант:

accessMode = flags & O_ACCMODE;

if (accessMode == O_WRONLY || accessMode == O_RDWR)

printf("file is writable\n");

Команду F_SETFL системного вызова fcntl() можно использовать для изменения некоторых флагов состояния открытого файла. К ним относятся O_APPEND, O_NONBLOCK, O_NOATIME, O_ASYNC и O_DIRECT. Попытки изменить другие флаги игнорируются. (В некоторых других реализациях UNIX системному вызову fcntl() разрешается изменять и другие флаги, например O_SYNC.)

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

• Файл был открыт не вызывающей программой, поэтому она не может управлять флагами, использованными в вызове open() (например, файл мог быть представлен одним из стандартных дескрипторов, открытых еще до запуска программы).

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

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

int flags;


flags = fcntl(fd, F_GETFL);

if (flags == -1)

errExit("fcntl");

flags |= O_APPEND;

if (fcntl(fd, F_SETFL, flags) == -1)

errExit("fcntl");


5.4. Связь файловых дескрипторов с открытыми файлами

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

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

• таблицу дескрипторов файлов для каждого процесса;

• общесистемную таблицу дескрипторов открытых файлов;

• таблицу индексных дескрипторов файловой системы.

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

• набор флагов, управляющих работой файлового дескриптора (такой флаг всего один — флаг закрытия при выполнении — close-on-exec, и он будет рассмотрен в разделе 27.4);

• ссылку на дескриптор открытого файла.

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

• текущее файловое смещение (обновляемое системными вызовами read() и write() или явно изменяемое с помощью системного вызова lseek());

• флаги состояния при открытии файла (то есть аргумент flags системного вызова open());

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

• установки, относящиеся к вводу-выводу, управляемому сигналами (см. раздел 59.3);

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

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

• тип файла (например, обычный файл, сокет или FIFO-устройство) и права доступа;

• указатель на список блокировок, удерживаемых на этом файле;

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

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

Связь между дескрипторами файлов, дескрипцией открытых файлов и индексными дескрипторами показана на рис. 5.2. На этой схеме у двух процессов имеется несколько дескрипторов открытых файлов.

В процессе А два дескриптора — 1 и 20 — ссылаются на один и тот же дескриптор открытого файла (с пометкой 23). Такая ситуация может возникать в результате вызова dup(), dup2() или fcntl() (см. раздел 5.5).

Дескриптор 2 процесса А и дескриптор 2 процесса Б ссылаются на один и тот же файловый дескриптор (73). Этот сценарий может сложиться после вызова fork() (то есть процесс А является родительским по отношению к процессу Б или наоборот) либо при условии, что один процесс передал открытый дескриптор другому процессу, используя доменный сокет UNIX (см. подраздел 57.13.3).

И наконец, можно увидеть, что дескриптор 0 процесса А и дескриптор 3 процесса Б ссылаются на различные дескрипторы открытых файлов, но эти дескрипции ссылаются на одну и ту же запись в таблице индексных дескрипторов (1976), то есть на один и тот же файл. Дело в том, что каждый процесс независимо вызвал open() для одного и того же файла. Похожая ситуация может возникнуть, если один и тот же процесс дважды откроет один и тот же файл.

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

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

• Аналогичные правила видимости применяются и к извлечению и изменению флагов состояния открытых файлов (например, O_APPEND, O_NONBLOCK и O_ASYNC) при использовании в системном вызове fcntl() операций F_GETFL и F_SETFL.

• В отличие от этого, флаги файлового дескриптора (то есть флаг закрытия при исполнении — close-on-exec) находятся в исключительном владении процесса и файлового дескриптора. Изменение этих флагов не влияет на другие файловые дескрипторы в одном и том же или в разных процессах.


5.5. Дублирование дескрипторов файлов

Использование синтаксиса перенаправления ввода-вывода (присущего Bourne shell) 2>&1 информирует оболочку о необходимости перенаправления стандартной ошибки (файловый дескриптор 2) в то же место, в которое выдается стандартный вывод (дескриптор файла 1). Таким образом, следующая команда станет (поскольку оболочка вычисляет направление ввода-вывода слева направо) отправлять и стандартный вывод, и стандартную ошибку в файл results.log:


$ ./myscript > results.log 2>&1


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


Оболочка перенаправляет стандартную ошибку, создавая дескриптор файла 2 дубликата дескриптора файла 1, так что он ссылается на ту же дескрипцию открытого файла, что и файловый дескриптор 1 (точно так же, как дескрипторы 1 и 20 процесса А ссылаются на одну и ту же дескрипцию открытого файла на рис. 5.2). Этого эффекта можно достичь, используя системные вызовы dup() и dup2().

Заметьте, что для оболочки недостаточно просто дважды открыть файл results.log: один раз с дескриптором 1 и один раз с дескриптором 2. Одна из причин состоит в том, что два файловых дескриптора не смогут совместно использовать указатель файлового смещения и это приведет к перезаписи вывода друг друга. Другая причина заключается в том, что файл может не быть дисковым. Рассмотрим следующую команду, отправляющую стандартную ошибку по тому же конвейеру, что и стандартный вывод:

$ ./myscript 2>&1 | less

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

#include <unistd.h>


int dup(int oldfd);

При успешном завершении возвращает новый файловый дескриптор, а при ошибке выдает –1

Предположим, что осуществляется следующий вызов:

newfd = dup(1);

Если предположить, что сложилась обычная ситуация, при которой оболочка открыла от имени программы файловые дескрипторы 0, 1 и 2, и не используются никакие другие дескрипторы, dup() откроет дубликат дескриптора 1, используя файловый дескриптор 3.

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

close(2); /* Высвобождение файлового дескриптора 2 */

newfd = dup(1); /* Повторное использование файлового дескриптора 2 */

Этот код работает, только если был открыт дескриптор 0. Чтобы упростить показанный выше код и обеспечить неизменное получение нужного нам файлового дескриптора, можно воспользоваться системным вызовом dup2().

#include <unistd.h>


int dup2(int oldfd, int newfd);

При успешном завершении возвращает новый файловый дескриптор, а при ошибке выдает –1

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

Предыдущие вызовы close() и dup() можно упростить, сведя их к следующему вызову:

dup2(1, 2);

Успешно завершенный вызов dup2() возвращает номер продублированного дескриптора (то есть значение, переданное в аргументе newfd).

Если аргумент oldfd не является допустимым файловым дескриптором, dup2() дает сбой с указанием на ошибку EBADF, и дескриптор, заданный в newfd, не закрывается. Если аргумент oldfd содержит допустимый файловый дескриптор и в аргументах oldfd и newfd хранится одно и то же значение, то dup2() не совершает никаких действий — дескриптор, указанный в newfd, не закрывается и dup2() возвращает в качестве результата своей работы значение аргумента newfd.

Еще один интерфейс, предоставляющий дополнительную гибкость для дублирования файловых дескрипторов, предусматривает использование операции F_DUPFD системного вызова fcntl():

newfd = fcntl(oldfd, F_DUPFD, startfd);

Этот вызов создает дубликат дескриптора, указанного в oldfd, путем использования наименьшего неиспользуемого дескриптора файла, который больше или равен номеру, заданному в startfd. Применяется, когда нужно обеспечить попадание нового дескриптора (newfd) в конкретный диапазон значений. Вызовы dup() и dup2() всегда могут быть записаны как вызовы close() и fcntl(), хотя они лаконичнее. (Следует также заметить, что некоторые коды ошибок в errno, возвращаемые dup2() и fcntl(), отличаются друг от друга — подробности см. на страницах руководств этих вызовов.)

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

Системный вызов dup3() выполняет ту же задачу, что и dup2(), но к нему добавляется новый аргумент, flags, который является битовой маской, изменяющей поведение системного вызова.

#define _GNU_SOURCE

#include <unistd.h>


int dup3(int oldfd, int newfd, int flags);

При успешном завершении возвращает новый файловый дескриптор, а при ошибке выдает –1

В настоящее время dup3() поддерживает один флаг — O_CLOEXEC, заставляющий ядро установить флаг закрытия при выполнении (FD_CLOEXEC) для нового файлового дескриптора. Польза от применения этого флага такая же, как от флага O_CLOEXEC системного вызова open(), рассмотренного в разделе 4.3.1.

Системный вызов dup3() появился в Linux 2.6.27 и характерен только для Linux.

Начиная с версии Linux 2.6.24, в этой ОС также поддерживается дополнительная операция системного вызова fcntl(), предназначенная для дублирования файловых дескрипторов: F_DUPFD_CLOEXEC. Этот флаг делает то же самое, что и F_DUPFD, но дополнительно он устанавливает для нового файлового дескриптора флаг закрытия при выполнении (FD_CLOEXEC). Польза от этой операции обусловлена теми же причинами, что и применение флага O_CLOEXEC для системного вызова open(). Операция F_DUPFD_CLOEXEC не определена в SUSv3, но поддерживается в SUSv4.


5.6. Файловый ввод-вывод по указанному смещению: pread() и pwrite()

Системные вызовы pread() и pwrite() работают практически так же, как read() и write(), за исключением того, что файловый ввод-вывод осуществляется с места, указанного значением offset, а не с текущего файлового смещения. Эти вызовы не изменяют файлового смещения.

#include <unistd.h>


ssize_t pread(int fd, void *buf, size_t count, off_t offset);

Возвращает количество считанных байтов, 0 при EOF или –1 при ошибке

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

Возвращает количество записанных байтов или –1 при ошибке

Вызов pread() эквивалентен атомарному выполнению следующих вызовов:

off_t orig;


orig = lseek(fd, 0, SEEK_CUR); /* Сохранение текущего смещения */

lseek(fd, offset, SEEK_SET);

s = read(fd, buf, len);

lseek(fd, orig, SEEK_SET); /* Восстановление исходного файлового смещения */

Как для pread(), так и для pwrite() файл, ссылка на который дается в аргументе fd, должен быть пригодным для изменения смещения (то есть представлен файловым дескриптором, в отношении которого допустимо вызвать lseek()).

В частности, такие системные вызовы могут пригодиться в многопоточных приложениях. В главе 29 будет показано, что все потоки в процессе совместно используют одну и ту же таблицу файловых дескрипторов. Это означает, что файловое смещение для каждого открытого файла является для всех потоков глобальным. Используя pread() или pwrite(), несколько потоков могут одновременно осуществлять ввод-вывод в отношении одного и того же файлового дескриптора, без влияния тех изменений, которые производят в отношении файлового смещения другие потоки. Если попытаться воспользоваться вместо этого lseek() плюс read() (или write()), то мы создадим состояние гонки, подобной одной из тех, описание которых давалось при рассмотрении флага O_APPEND в разделе 5.1. (Системные вызовы pread() и pwrite() могут также пригодиться для устранения состояния гонки в приложениях, когда у нескольких процессов имеются файловые дескрипторы, ссылающиеся на одну и ту же дескрипцию открытого файла.)

При условии многократного выполнения вызовов lseek() с последующим файловым вводом-выводом системные вызовы pread() и pwrite() могут также предложить в некоторых случаях преимущества в производительности. Дело в том, что отдельный системный вызов pread() (или pwrite()) приводит к меньшим издержкам, чем два системных вызова: lseek() и read() (или write()). Но издержки, связанные с системными вызовами, обычно незначительны по сравнению со временем фактического выполнения ввода-вывода.


5.7. Ввод-вывод по принципу фрагментации-дефрагментации: readv() и writev()

Системные вызовы readv() и writev() выполняют фрагментированный ввод/вывод (scatter-gather I/O).

#include <sys/uio.h>


ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

Возвращает количество считанных байтов, 0 при EOF или –1 при ошибке

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

Возвращает количество записанных байтов или –1 при ошибке

За один системный вызов обрабатываются несколько таких буферов данных. Набор передаваемых буферов определяется массивом iov. Количество элементов в iov указывается в iovcnt. Каждый элемент в iov является структурой с такой формой:

struct iovec {

void *iov_base; /* Начальный адрес буфера */

size_t iov_len; /* Количество байтов для передачи в буфер или из него */

};

Согласно спецификации SUSv3, допускается устанавливать ограничение по количеству элементов в iov. Реализация может уведомить о своем ограничении, определив значение IOV_MAX в заголовочном файле <limits.h> или в ходе выполнения через возвращаемое значение вызова sysconf(_SC_IOV_MAX). (Вызов sysconf() рассматривается в разделе 11.2.) В спецификации SUSv3 требуется, чтобы это ограничение было не меньше 16. В Linux для IOV_MAX определено значение 1024, что соответствует ограничениям ядра на размер этого вектора (задается константой ядра UIO_MAXIOV).

При этом функции оболочки из библиотеки glibc для readv() и writev() незаметно выполняют дополнительные действия. Если системный вызов дает сбой по причине слишком большого значения iovcnt, функция-оболочка временно выделяет один буфер, чьего объема достаточно для хранения всех элементов, описанных iov, и выполняет вызов read() или write() (см. далее тему о возможной реализации writev() с использованием write()).

На рис. 5.3 показан пример взаимосвязанности аргументов iov и iovcnt, а также буферов, на которые они ссылаются.


Фрагментированный ввод

Системный вызов readv() выполняет фрагментированный ввод: он считывает непрерывную последовательность байтов из файла, ссылка на который дается в файловом дескрипторе fd, и помещает («фрагментирует») эти байты в буферы, указанные аргументом iov. Каждый из буферов, начиная с того, что определен элементом iov[0], полностью заполняется, прежде чем readv() переходит к следующему буферу.



Рис. 5.3. Пример массива iov и связанных с ним буферов


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

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

Пример использования вызова readv() показан в листинге 5.2.

Будем придерживаться следующего соглашения: если название файла состоит из префикса t_ и имени функции(…) (например, t_readv.c в листинге 5.2), это значит, что программа главным образом демонстрирует работу одного системного вызова или библиотечной функции.


Листинг 5.2. Выполнение фрагментированного ввода с помощью readv()

fileio/t_readv.c

#include <sys/stat.h>

#include <sys/uio.h>

#include <fcntl.h>

#include "tlpi_hdr.h"


int

main(int argc, char *argv[])

{

int fd;

struct iovec iov[3];

struct stat myStruct; /* Первый буфер */

int x; /* Второй буфер */

#define STR_SIZE 100

char str[STR_SIZE]; /* Третий буфер */

ssize_t numRead, totRequired;

if (argc!= 2 || strcmp(argv[1], "-help") == 0)

usageErr("%s file\n", argv[0]);

fd = open(argv[1], O_RDONLY);

if (fd == -1)

errExit("open");

totRequired = 0;

iov[0].iov_base = &myStruct;

iov[0].iov_len = sizeof(struct stat);

totRequired += iov[0].iov_len;

iov[1].iov_base = &x;

iov[1].iov_len = sizeof(x);

totRequired += iov[1].iov_len;

iov[2].iov_base = str;

iov[2].iov_len = STR_SIZE;

totRequired += iov[2].iov_len;


numRead = readv(fd, iov, 3);

if (numRead == -1)

errExit("readv");

if (numRead < totRequired)

printf("Read fewer bytes than requested\n");


printf("total bytes requested: %ld; bytes read: %ld\n",

(long) totRequired, (long) numRead);

exit(EXIT_SUCCESS);

}

fileio/t_readv.c


Дефрагментированный вывод

Системный вызов writev() выполняет дефрагментированный вывод. Он объединяет («дефрагментирует») данные из всех буферов, указанных в аргументе iov, и записывает их в виде непрерывной последовательности байтов в файл, ссылка на который находится в файловом дескрипторе fd. Дефрагментация буферов происходит в порядке следования элементов массива, начиная с буфера, определяемого элементом iov[0].

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

Как и в случае с write(), возможна частичная запись. Поэтому нужно проверять значение, возвращаемое writev(), чтобы увидеть, все ли запрошенные байты были записаны.

Главными преимуществами readv() и writev() являются удобство и скорость. Например, вызов writev() можно заменить:

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

• либо серией вызовов write(), выводящих данные из отдельных буферов.

Первый из вариантов, будучи семантическим эквивалентом использования writev(), неудобен (и неэффективен), так как требуется выделять буферы и копировать данные в пользовательском пространстве. Второй вариант не является семантическим эквивалентом одному вызову writev(), так как вызовы write() не выполняются атомарно. Более того, выполнение одного системного вызова writev() обходится дешевле выполнения нескольких вызовов write() (вспомним раздел 3.1).


Выполнение фрагментированного ввода-вывода по указанному смещению

В Linux 2.6.30 также были добавлены два новых системных вызова, сочетающих в себе функциональные возможности фрагментированного ввода-вывода с возможностью выполнения ввода-вывода по указанному смещению: preadv() и pwritev(). Это нестандартные системные вызовы, которые также доступны в современных BSD-системах.

#define _BSD_SOURCE

#include <sys/uio.h>


ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);

Возвращает количество считанных байтов, 0 при EOF или –1 при ошибке

ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);

Возвращает количество записанных байтов или –1 при ошибке

Системные вызовы preadv() и pwritev() выполняют ту же задачу, что и readv() и writev(), но осуществляют ввод/вывод в отношении того места в файле, которое указано смещением (наподобие pread() и pwrite()). Эти системные вызовы не меняют смещение файла.

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


5.8. Усечение файла: truncate() и ftruncate()

Системные вызовы truncate() и ftruncate() устанавливают для файла размер, соответствующий значению, указанному в аргументе length.

#include <unistd.h>


int truncate(const char *pathname, off_t length);

int ftruncate(int fd, off_t length);

Оба возвращают 0 при успешном завершении или –1 при ошибке

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

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

Если значение аргумента length для ftruncate() превышает текущий размер файла, в спецификации SUSv3 разрешается проявлять один из двух вариантов поведения: либо файл расширяется (как в Linux), либо системный вызов возвращает ошибку. XSI-совместимые системы должны принять первую линию поведения. В SUSv3 требуется, чтобы truncate() всегда расширял файл, если значение length превышает его текущий размер.

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


5.9. Неблокирующий ввод-вывод

Указание флага O_NONBLOCK при открытии файла служит двум целям.

• Если файл не может быть открыт немедленно, вызов open() вместо блокирования возвращает ошибку. Одним из случаев, при котором open() может проводить блокировку, является использование этого системного вызова в отношении FIFO-устройств (см. раздел 44.7).

• После успешного завершения open() дальнейшие операции ввода-вывода также являются неблокирующими. Если системный вызов ввода-вывода не может завершиться немедленно, то либо выполняется частичное портирование данных, либо системный вызов дает сбой с выдачей одной из ошибок: EAGAIN или EWOULDBLOCK. Какая из ошибок будет возвращена, зависит от системного вызова. В Linux, как и во многих реализациях UNIX, эти две константы ошибок синонимичны.

Неблокирующий режим может использоваться с устройствами (например, с терминалами и псевдотерминалами), конвейерами, FIFO-устройствами и сокетами. (Поскольку при использовании open() дескрипторы для конвейеров и сокетов не получаются, этот флаг должен устанавливаться при использовании операции F_SETFL системного вызова fcntl(), рассматриваемого в разделе 5.3.)

Для обычных файлов указание флага O_NONBLOCK, как правило, не требуется, поскольку буферный кэш ядра гарантирует, что ввод-вывод в отношении обычных файлов, как описывается в разделе 13.1, не блокируется. Но O_NONBLOCK оказывает влияние на обычные файлы, когда используется обязательная блокировка файлов (см. раздел 51.4).

Неблокирующий ввод-вывод будет также рассматриваться в разделе 44.9 и в главе 59.

Исторически реализации, берущие начало из System V, предоставляли флаг O_NDELAY, имеющий сходную с O_NONBLOCK семантику. Основное отличие состояло в том, что неблокирующий системный вызов write() в System V возвращал 0, если write () не мог быть завершен, а неблокирующий вызов read() возвращал 0, если ввод был недоступен. Это поведение создавало проблемы для read(), поскольку было неотличимо от условий, при которых встречался конец файла. Поэтому в первом стандарте POSIX.1 был введен флаг O_NONBLOCK. В некоторых реализациях UNIX по-прежнему предоставляется флаг O_NDELAY со старой семантикой. В Linux определена константа O_NDELAY, но она является синонимом O_NONBLOCK.


5.10. Ввод-вывод, осуществляемый в отношении больших файлов

Тип данных off_t, используемый для хранения файлового смещения, обычно реализуется как длинное целое число со знаком. (Тип данных со знаком нужен потому, что при ошибке возвращается –1.) На 32-разрядных архитектурах (таких как x86-32) это будет ограничивать размер файлов 231 — 1 байтами (то есть 2 Гбайт).

Но емкость дисковых накопителей давным-давно преодолела это ограничение, и перед 32-разрядными реализациями Unix встала необходимость работать с файлами, превышающими этот размер. Поскольку проблема затрагивала многие реализации, группа поставщиков UNIX приняла решение объединить свои усилия на саммите, посвященном работе с большими файлами — Large File Summit (LFS), с целью улучшения спецификации SUSv2. Планировалось добавить дополнительные функциональные возможности, необходимые для доступа к большим файлам. Усовершенствования, предложенные в рамках LFS, мы рассмотрим в текущем разделе. (Полная LFS-спецификация, работа над которой завершилась в 1996 году, находится по адресу http://opengroup.org/platform/lfs.html.)

В Linux LFS-поддержка для 32-разрядных систем была предоставлена, начиная с версии ядра 2.4 (для чего также требуется версия glibc 2.2 или выше). Кроме того, соответствующая файловая система должна поддерживать большие файлы. Эта поддержка предоставляется большинством свойственных для Linux файловых систем, в отличие от некоторых несвойственных систем (характерными примерами могут послужить разработанные Microsoft файловые системы VFAT и NFSv2, накладывающие жесткие ограничения 2 Гбайт на файл, независимо от того, используются LFS-расширения или нет).

Поскольку для длинных целых чисел на 64-разрядных архитектурах (например, x86-64, Alpha, IA-64) используются 64 бита, на такие архитектуры вообще не влияют ограничения, для преодоления которых были разработаны LFS-усовершенствования. И все же особенности реализации некоторых свойственных Linux файловых систем предполагают, что теоретический максимальный размер может быть меньше 263 — 1 даже в 64-разрядных системах. В большинстве случаев эти ограничения существенно выше, чем современные объемы дисков, поэтому они не накладывают значимых ограничений на размеры файлов.

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

• Воспользоваться альтернативным API, поддерживающим большие файлы. Он был разработан LFS в качестве «переходного расширения» для спецификации Single UNIX Specification. В результате наличие этого интерфейса не требуется в системах, соответствующих SUSv2 или SUSv3, но многие подобные системы его предоставляют. На данный момент этот подход уже устарел.

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


Переходный API LFS

Чтобы воспользоваться переходным API LFS, нужно при компилировании своей программы определить макрос проверки возможностей _LARGEFILE64_SOURCE либо в командной строке, либо внутри исходного файла перед включением любых заголовочных файлов. Этот API предоставляет функции, способные работать с 64-разрядными размерами файлов и файловыми смещениями. У этих функций такие же имена, как и у их 32-разрядных двойников, но с добавлением к именам функций суффикса 64. К числу таких функций относятся fopen64(), open64(), lseek64(), truncate64(), stat64(), mmap64() и setrlimit64(). (Некоторые из их 32-разрядных двойников уже рассматривались, другие же будут описаны в этой главе чуть позже.)

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

fd = open64(name, O_CREAT | O_RDWR, mode);

if (fd == -1)

errExit("open");

Вызов open64() эквивалентен указанию флага O_LARGEFILE при вызове open(). Попытки открыть файл, размер которого превышает 2 Гбайт, с помощью open() без этого флага приведут к возвращению ошибки.

Вдобавок к вышеупомянутым функциям переходный API добавляет несколько типов данных, в числе которых:

• struct stat64: аналог структуры stat (см. раздел 15.1), позволяющий работать с большими файлами;

• off64_t: 64-разрядный тип для представления файловых смещений.

Как показано в листинге 5.3, тип данных off64_t используется (кроме всего прочего) с функцией lseek64(). Демонстрируемая там программа получает два аргумента командной строки: имя открываемого файла и целочисленное значение, указывающее файловое смещение. Программа открывает указанный файл, переходит по заданному файловому смещению, а затем записывает строку. В следующей сессии командной оболочки показано использование программы для перехода в файле по очень большому файловому смещению (больше 10 Гбайт) с дальнейшей записью нескольких байтов:

$ ./large_file x 10111222333

$ ls — l x Проверка размера получившегося в результате файла

— rw- 1 mtk users 10111222337 Mar 4 13:34 x


Листинг 5.3. Обращение к большим файлам

fileio/large_file.c

#define _LARGEFILE64_SOURCE

#include <sys/stat.h>

#include <fcntl.h>

#include "tlpi_hdr.h"


int

main(int argc, char *argv[])

{

int fd;

off64_t off;


if (argc!= 3 || strcmp(argv[1], "-help") == 0)

usageErr("%s pathname offset\n", argv[0]);

fd = open64(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);

if (fd == -1)

errExit("open64");

off = atoll(argv[2]);

if (lseek64(fd, off, SEEK_SET) == -1)

errExit("lseek64");


if (write(fd, "test", 4) == -1)

errExit("write");

exit(EXIT_SUCCESS);

}

fileio/large_file.c


Макрос _FILE_OFFSET_BITS

Для получения функциональных возможностей LFS рекомендуется определить макрос _FILE_OFFSET_BITS со значением 64 при компиляции программы. Один из способов предусматривает использование ключа командной строки при запуске компилятора языка C:

$ cc — D_FILE_OFFSET_BITS=64 prog.c

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

#define _FILE_OFFSET_BITS 64

Это определение автоматически переводит использование всех соответствующих 32-разрядных функций и типов данных на применение их 64-разрядных двойников. Так, например, вызов open() фактически превращается в вызов open64(), а тип данных off_t определяется в виде 64-разрядного длинного целого числа. Иными словами, мы можем перекомпилировать существующую программу для работы с большими файлами, не внося при этом никаких изменений в исходный код.

Добавление макроса проверки возможностей _FILE_OFFSET_BITS явно проще применения переходного API LFS, но этот подход зависит от чистоты написания приложений (например, от правильного использования off_t для объявления переменных, хранящих файловые смещения, вместо применения свойственного языку C целочисленного типа).

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

При попытке обращения к большому файлу с использованием 32-разрядных функций (то есть из программы, скомпилированной без установки для _FILE_OFFSET_BITS значения 64) можно столкнуться с ошибкой EOVERFLOW. Например, она может быть выдана при попытке использовать 32-разрядную версию функции stat() (см. раздел 15.1) для извлечения информации о файле, размер которого превышает 2 Гбайт.


Передача значений off_t вызовам printf()

Надо отметить, что LFS-расширения не решают для нас одну проблему: как выбрать способ передачи значений off_t вызовам printf(). В подразделе 3.6.2 было отмечено, что портируемый метод, который выводит значения одного из предопределенных типов системных данных (например, pid_t или uid_t), заключается в приведении значения к типу long и использовании для printf() спецификатора %ld. Но если применяются LFS-расширения, для типа данных off_t этого зачастую недостаточно, поскольку он может быть определен как тип, который длиннее long, обычно как long long. Поэтому для вывода значения типа off_t оно приводится к long long, а для printf() задается спецификатор %lld:

#define _FILE_OFFSET_BITS 64


off_t offset; /* Должен быть 64 бита, а это размер 'long long' */


/* Некоторый код, присваивающий значение 'offset' */

printf("offset=%lld\n", (long long) offset);

Подобные замечания применимы и к родственному типу данных blkcnt_t, используемому в структуре stat (рассматриваемой в разделе 15.1).

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


5.11. Каталог /dev/fd

Каждому процессу ядро предоставляет специальный виртуальный каталог /dev/fd. Он содержит имена файлов вида /dev/fd/n, где n является номером, соответствующим одному из дескрипторов файла, открытого для этого процесса. К примеру, /dev/fd/0 является для процесса стандартным вводом. (Свойство каталога /dev/fd в SUSv3 не указано, но некоторые другие реализации UNIX его предоставляют.)

В некоторых системах (но не в Linux) открытие одного из файлов в каталоге /dev/fd эквивалентно дублированию соответствующего файлового дескриптора. Таким образом, следующие инструкции эквивалентны друг другу:

fd = open("/dev/fd/1", O_WRONLY);

fd = dup(1); /* Дублирование стандартного вывода */

Аргумент flags вызова open() интерпретируется, поэтому следует позаботиться об указании точно такого же режима доступа, который был использован исходным дескриптором. Указывать другие флаги, такие как O_CREAT, в данном контексте не имеет смысла (они просто игнорируются).

В Linux открытие одного из файлов в /dev/fd эквивалентно повторному открытию исходного файла. Иначе говоря, новый файловый дескриптор связан с новым описанием открытого файла (и, следовательно, имеет различные флаги состояния файла и смещение файла).

Фактически /dev/fd является символьной ссылкой на характерный для Linux каталог /proc/self/fd. Он представляет собой частный случай свойственных для Linux каталогов /proc/PID/fd, в каждом из которых хранятся символьные ссылки, соответствующие всем файлам, содержащимся процессом в открытом состоянии.

В программах файлы в каталоге /dev/fd редко используются. Наиболее часто они применяются в оболочке. Многие из доступных пользователю команд принимают в качестве аргументов имена файлов, и иногда удобно соединить эти команды с помощью конвейера, чтобы использовать стандартный ввод или вывод в качестве такого аргумента. Для этой цели некоторые программы (например, diff, ed, tar и comm) задействуют аргумент, состоящий из одиночного дефиса (-), означающего: «в качестве файла, имя которого должно быть указано в аргументах, использовать стандартный ввод или вывод (что больше соответствует)». Так, для сравнения списка файлов из ls с ранее созданным списком файлов можно набрать такую команду:

$ ls | diff — oldfilelist

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

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

$ ls | diff /dev/fd/0 oldfilelist

Для удобства в качестве символьных ссылок на /dev/fd/0, /dev/fd/1 и /dev/fd/2 соответственно предоставляются имена /dev/stdin, /dev/stdout и /dev/stderr.


5.12. Создание временных файлов

Некоторые программы нуждаются в создании временных файлов, используемых только при выполнении программы, а при завершении программы такие файлы должны быть удалены. Например, временные файлы создаются в ходе компиляции многими компиляторами. Для этих целей в GNU-библиотеке языка C предоставляется несколько библиотечных функций. Здесь будут рассмотрены две такие функции: mkstemp() и tmpfile().

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

#include <stdlib.h>


int mkstemp(char *template);

При успешном завершении возвращает новый файловый дескриптор, а при ошибке выдает –1

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

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

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

int fd;

char template[] = "/tmp/somestringXXXXXX";


fd = mkstemp(template);

if (fd == -1)

errExit("mkstemp");

printf("Generated filename was: %s\n", template);

unlink(template); /* Имя тут же исчезает, но файл удаляется только после close() */


/* Использование системных вызовов ввода-вывода — read(), write() и т. д. */

if (close(fd) == -1)

errExit("close");

Для создания уникальных имен файлов могут также применяться функции tmpnam(), tempnam() и mktemp(). Но их использования следует избегать, поскольку они могут создавать в приложении бреши в системе безопасности. Дополнительные подробности об этих функциях можно найти на страницах руководства.

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

#include <stdio.h>


FILE *tmpfile(void);

Возвращает указатель на файл при успешном завершении или NULL при ошибке

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


5.13. Резюме

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

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

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

Были описаны некоторые системные вызовы, расширяющие функциональные возможности обычных системных вызовов read() и write(). Системные вызовы pread() и pwrite() выполняют ввод/вывод в указанном месте файла, не изменяя при этом файлового смещения. Системные вызовы readv() и writev() выполняют фрагментированный ввод/вывод. Вызовы preadv() и pwritev() сочетают в себе функциональные возможности фрагментированного ввода-вывода с возможностью выполнять ввод/вывод в указанном месте файла.

Системные вызовы truncate() и ftruncate() могут использоваться для уменьшения размера файла, для избавления от избыточных байтов или для наращивания размера путем добавления файловых дыр, заполненных нулевыми байтами.

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

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

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

Функции mkstemp() и tmpfile() позволяют приложению создавать временные файлы.


5.14. Упражнения

5.1. Если у вас есть доступ к 32-разрядной системе Linux, измените программу в листинге 5.3 под использование стандартных системных вызовов файлового ввода-вывода (open() и lseek()) и под тип данных off_t. Откомпилируйте программу с установленным для макроса _FILE_OFFSET_BITS значением 64 и протестируйте ее, показав, что она может успешно создавать большие файлы.

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

5.3. Это упражнение демонстрирует необходимость атомарности, гарантированной при открытии файла с флагом O_APPEND. Напишите программу, получающую до трех аргументов командной строки:

$ atomic_append filename num-bytes [x]

Эта программа должна открыть файл с именем, указанным в аргументе filename (создав его при необходимости), и дополнить его количеством байтов, заданным в аргументе num-bytes, используя вызов write() для побайтовой записи. По умолчанию программа должна открыть файл с флагом O_APPEND, но, если есть третий аргумент командной строки (x), флаг O_APPEND должен быть опущен. При этом, вместо того чтобы добавлять байты, программа должна выполнять перед каждым вызовом write() вызов lseek(fd, 0, SEEK_END). Запустите одновременно два экземпляра этой программы без аргумента x для записи одного миллиона байтов в один и тот же файл:

$ atomic_append f1 1000000 & atomic_append f1 1000000

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

$ atomic_append f2 1000000 x & atomic_append f2 1000000 x

Выведите на экран размеры файлов f1 и f2, воспользовавшись командой ls — l, и объясните разницу между ними.

5.4. Реализуйте функции dup() и dup2(), используя функцию fcntl() и, там где это необходимо, функцию close(). (Тот факт, что dup2() и fcntl() в некоторых случаях возникновения ошибок возвращают различные значения errno, можно проигнорировать.) Для dup2() не забудьте учесть особый случай, когда oldfd равен newfd. В этом случае нужно проверить допустимость значения oldfd, что можно сделать, к примеру, проверкой успешности выполнения вызова fcntl(oldfd, F_GETFL). Если значение oldfd недопустимо, функция должна возвратить –1, а значение errno должно быть установлено в EBADF.

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

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

fd1 = open(file, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);

fd2 = dup(fd1);

fd3 = open(file, O_RDWR);

write(fd1, "Hello,", 6);

write(fd2, " world", 6);

lseek(fd2, 0, SEEK_SET);

write(fd1, "HELLO,", 6);

write(fd3, "Gidday", 6);

5.7. Реализуйте функции readv() и writev(), используя системные вызовы read(), write() и подходящие функции из пакета malloc (см. подраздел 7.1.2).

6. Процессы

В этой главе будет рассмотрена структура процесса, при этом особое внимание мы уделим структуре и содержимому виртуальной памяти процесса. Будут также изучены некоторые атрибуты процесса. В следующих главах мы рассмотрим другие атрибуты процесса (например, идентификаторы процесса в главе 9 и приоритеты процесса и его диспетчеризацию в главе 35). В главах 24–27 описываются особенности создания процесса, методы прекращения его работы и методы создания процессов для выполнения новых программ.


6.1. Процессы и программы

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

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

• Идентификационный признак двоичного формата. Каждый программный файл включает в себя метаинформацию с описанием формата исполняемого файла. Это позволяет ядру интерпретировать всю остальную содержащуюся в файле информацию. Изначально для исполняемых файлов UNIX было предусмотрено два широко используемых формата: исходный формат a.out (assembler output — вывод на языеке ассемблера) и появившийся позже более сложный общий формат объектных файлов — COFF (Common Object File Format). В настоящее время в большинстве реализаций UNIX (включая Linux) применяется формат исполняемых и компонуемых файлов — Executable and Linking Format (ELF), предоставляющий множество преимуществ по сравнению со старыми форматами.

Машинный код. В нем закодирован алгоритм программы.

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

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

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

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

• Другая информация. В программном файле есть и другая информация, описывающая способ построения процесса.

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

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

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


6.2. Идентификатор процесса и идентификатор родительского процесса

У каждого процесса есть идентификатор (process ID — PID), положительное целое число, уникальным образом идентифицирующее процесс в системе. Идентификаторы процессов используются и возвращаются различными системными вызовами. Например, системный вызов kill() (см. раздел 20.5) позволяет отправить сигнал процессу с указанным идентификатором. PID также используется при необходимости создания идентификатора, который будет уникальным для процесса. Характерный пример — применение идентификатора процесса как части уникального для процесса имени файла.

Идентификатор вызывающего процесса возвращается системным вызовом getpid().

#include <unistd.h>


pid_t getpid(void);

Всегда успешно возвращает идентификатор вызывающего процесса

Тип данных pid_t, используемый для значения, возвращаемого getpid(), является целочисленным типом. Он определен в спецификации SUSv3 для хранения идентификаторов процессов.

За исключением нескольких системных процессов, таких как init (чей PID равен 1), между программой и идентификатором процесса, созданным для ее выполнения, нет никакой фиксированной связи.

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

По достижении числа 32 767 счетчик идентификаторов процессов переустанавливается на значение 300, а не на 1. Так происходит потому, что многие идентификаторы процессов с меньшими номерами находятся в постоянном использовании системными процессами и демонами, и время на поиск неиспользуемого PID в этом диапазоне будет потрачено впустую.

В Linux 2.4 и более ранних версиях ограничение идентификаторов процессов в 32 767 единиц определено в константе ядра PID_MAX. Начиная с Linux 2.6, ситуация изменилась. Хотя исходный верхний порог для идентификаторов процессов остался прежним — 32 767, его можно изменить, задав значение в характерном для Linux файле /proc/sys/kernel/pid_max (которое на единицу больше, чем максимально возможное количество идентификаторов процессов). На 32-разрядной платформе максимальным значением для этого файла является 32 768, но на 64-разрядной платформе оно может быть установлено в любое значение вплоть до 222 (приблизительно 4 миллиона), позволяя справиться с очень большим количеством процессов.

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

#include <unistd.h>


pid_t getppid(void);

Всегда успешно возвращает идентификатор родительского процесса для того процесса, который его вызвал

По сути, имеющийся у каждого процесса атрибут идентификатора родительского процесса представляет древовидную связь всех процессов в системе. Родитель каждого процесса имеет собственного родителя и т. д., возвращаясь в конечном итоге к процессу 1, init, предку всех процессов. (Это «родовое дерево» может быть просмотрено с помощью команды pstree(1).)

Если дочерний процесс становится «сиротой» из-за завершения работы «породившего» его родительского процесса, то он оказывается приемышем у процесса init и последующий за этим вызов getppid(), сделанный из дочернего процесса, возвратит результат 1 (см. раздел 26.2).

Родитель любого процесса может быть найден при просмотре поля PPid, предоставляемого характерным для Linux файлом /proc/PID/status.


6.3. Структура памяти процесса

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

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

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

Сегмент неинициализированных данных — содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система инициализирует всю память в этом сегменте значением 0. По историческим причинам этот сегмент часто называют bss. Его имя произошло из старого ассемблерного мнемонического термина block started by symbol («блок, начинающийся с символа»). Основная причина помещения прошедших инициализацию глобальных и статических переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные. Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.

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

• Динамическая память, или куча, — область, из которой память (для переменных) может динамически выделяться в ходе выполнения программы. Верхний конец кучи называют крайней точкой программы (program break).

Не такими популярными, но более наглядными маркировками для сегментов инициализированных и неинициализированных данных являются сегмент данных, инициализированных пользователем (user-initialized data segment), и сегмент данных с нулевой инициализацией (zero-initialized data segment).

Команда size(1) выводит размеры текстового сегмента, сегментов инициализированных и неинициализированных (bss) данных двоичной исполняемой программы.

Термин «сегмент», который употребляется в основном тексте, не нужно путать с аппаратной сегментацией, используемой в некоторой аппаратной архитектуре, например в x86-32. В нашем случае сегменты представляют собой логические разделения виртуальной памяти процесса в системах UNIX. Иногда вместо сегмента употребляется термин «раздел» (section), поскольку он более соответствует терминологии, используемой в настоящее время повсеместно согласно ELF-спецификации для форматов исполняемого файла.

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

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


Листинг 6.1. Размещение переменных программы в сегментах памяти процесса

proc/mem_segments.c

#include <stdio.h>

#include <stdlib.h>


char globBuf[65536]; /* Сегмент неинициализированных данных */

int primes[] = { 2, 3, 5, 7 }; /* Сегмент инициализированных данных */


static int

square(int x) /* Размещается в фрейме для square() */

{

int result; /* Размещается в фрейме для square() */

result = x * x;

return result; /* Возвращаемое значение передается через регистр */

}


static void

doCalc(int val) /* Размещается в фрейме для doCalc() */

{

printf("The square of %d is %d\n", val, square(val));

if (val < 1000) {

int t; /* Размещается в фрейме для doCalc() */

t = val * val * val;

printf("The cube of %d is %d\n", val, t);

}

}


int

main(int argc, char *argv[]) /* Размещается в фрейме для main() */

{

static int key = 9973; /* Сегмент инициализированных данных */

static char mbuf[10240000]; /* Сегмент неинициализированных данных */

char *p; /* Размещается в фрейме для main() */

p = malloc(1024); /* Указывает на память в сегменте кучи */

doCalc(key);

exit(EXIT_SUCCESS);

}

proc/mem_segments.c

Двоичный интерфейс приложений — Application Binary Interface (ABI) представляет собой набор правил, регулирующих порядок обмена информацией между двоичной исполняемой программой в ходе ее выполнения и каким-либо сервисом (например, ядром или библиотекой). Помимо всего прочего, ABI определяет, какие регистры и места в стеке используются для обмена этой информацией и какой смысл придается обмениваемым значениям. Программа, единожды скомпилированная в соответствии с требованием некоторого ABI, должна запускаться в любой системе, предоставляющей точно такой же ABI. Это отличается от стандартизированного API (например, SUSv3), гарантирующего портируемость только для приложений, скомпилированных из исходного кода.

Хотя это и не описано в SUSv3, среда программы на языке C во многих реализациях UNIX (включая Linux) предоставляет три глобальных идентификатора: etext, edata и end. Они могут использоваться из программы для получения адресов следующего байта соответственно за концом текста программы, за концом сегмента инициализированных данных и за концом сегмента неинициализированных данных. Чтобы воспользоваться этими идентификаторами, их нужно явным образом объявить:

extern char etext, edata, end;

/* К примеру, &etext сообщает адрес первого байта после окончания

текста программы/начала инициализированных данных */

На рис. 6.1 показано расположение различных сегментов памяти в архитектуре x86-32. Пространство с пометкой argv, охватывающее верхнюю часть этой схемы, содержит аргументы командной строки программы (которые в C доступны через аргумент argv функции main()) и список переменных среды процесса (который вскоре будет рассмотрен). Шестнадцатеричные адреса, приведенные в схеме, могут варьироваться в зависимости от конфигурации ядра и ключей компоновки программы. Области, закрашенные серым цветом, представляют собой недопустимые диапазоны в виртуальном адресном пространстве процесса, то есть области, для которых не созданы таблицы страниц (см. далее раздел, посвященный управлению виртуальной памятью).



Рис. 6.1. Типичная структура памяти процесса в Linux/x86-32


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

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

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

• Пространственная локальность, которая характеризуется присущей программам тенденцией ссылаться на адреса памяти, близкие к тем, к которым недавно обращались (из-за последовательного характера обработки инструкций и иногда последовательного характера обработки структур данных).

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

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

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

В системах x86-32 размер страницы составляет 4096 байт. В некоторых других реализациях Linux используются страницы больших размеров. Например, в Alpha — страницы размером 8192 байт, а в IA-64 — изменяемый размер страниц, обычно с исходным объемом 16 384 байт. Программа может определить размер страницы виртуальной памяти системы с помощью вызова sysconf(_SC_PAGESIZE), рассматриваемого в разделе 11.2.

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

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



Рис. 6.2. Общий вид виртуальной памяти


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

• когда стек разрастается вниз, выходя за ранее обозначенные ограничения;

• когда память выделяется в куче или высвобождается в ней путем подъема крайней точки программы с использованием вызовов brk(), sbrk() или семейства функций malloc (см. главу 7);

• когда области совместно используемой памяти (System V) прикрепляются с помощью вызова shmat() и открепляются вызовом shmdt();

• когда отображение памяти создается с применением вызова mmap() и убирается с помощью munmap() (см. главу 45).

Реализация виртуальной памяти требует аппаратной поддержки в виде блока управления страничной памятью — Paged Memory Management Unit (PMMU). Блок PMMU переводит каждую ссылку на адрес виртуальной памяти в соответствующий адрес физической памяти и извещает ядро об ошибке отсутствия страницы, когда конкретный адрес виртуальной памяти ссылается на страницу, отсутствующую в оперативной памяти.

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

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

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

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

• Для явного запроса областей совместно используемой памяти с другими процессами процессы могут задействовать системные вызовы shmget() и mmap(). Это делается в целях обмена данными между процессами.

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

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

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

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


6.5. Стек и стековые фреймы

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

Хотя стек растет вниз, мы все равно называем растущий край стека вершиной, поскольку, абстрактно говоря, он таковым и является. Фактическое направление роста относится к подробностям аппаратной реализации. В одной из реализаций Linux, HP PA-RISC, используется стек, растущий вверх.

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

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

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

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

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

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



Рис. 6.3. Пример стека процесса


6.6. Аргументы командной строки (argc, argv)

У каждой программы на языке C должна быть функция по имени main(), с которой и начинается выполнение программы. Когда программа выполняется, к аргументам командной строки (отдельным словам, анализируемым оболочкой) открывается доступ через два аргумента функции main(). Первый аргумент, int argc, показывает, сколько есть аргументов командной строки. Второй аргумент, char *argv[], является массивом указателей на аргументы командной строки, каждый из которых представляет символьную строку, завершающуюся нулевым байтом. Первая из этих строк, argv[0], является (по традиции) именем самой программы. Список указателей в argv завершается указателем со значением NULL (то есть argv[argc] имеет значение NULL).

Поскольку в argv[0] содержится имя, под которым программа была вызвана, это можно использовать для выполнения полезного приема. На одну и ту же программу можно создать несколько ссылок (то есть имен для нее), а затем заставить программу заглянуть в argv[0] и выполнить различные действия в зависимости от имени, используемого для ее вызова. Пример такой технологии предоставляется командами gzip(1), gunzip(1) и zcat(1): в некоторых дистрибутивах это ссылки на один и тот же исполняемый файл. (Применяя эту технологию, нужно быть готовым обработать вызов пользователя программы по ссылке с именем, не входящим в перечень ожидаемых имен.)

На рис. 6.4 продемонстрирован пример структур данных, связанных с argc и argv, при выполнении программы, приведенной в листинге 6.2. На этой схеме завершающие нулевые байты в конце каждой строки показаны с использованием принятой в языке C записи \0.



Рис. 6.4. Значения argc и argv для команды necho hello world


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


Листинг 6.2. Повторение на экране аргументов командной строки

proc/necho.c

#include "tlpi_hdr.h"

int

main(int argc, char *argv[])

{

int j;

for (j = 0; j < argc; j++)

printf("argv[%d] = %s\n", j, argv[j]);

exit(EXIT_SUCCESS);

}

proc/necho.c

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

char **p;


for (p = argv; *p!= NULL; p++)

puts(*p);

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

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

• Аргументы командной строки любого процесса могут быть считаны через характерный для Linux файл /proc/PID/cmdline, в котором каждый аргумент завершается нулевым байтом. (Программа может получить доступ к аргументам своей собственной командной строки через файл /proc/self/cmdline.)

• GNU-библиотека C предоставляет две глобальные переменные, который могут применяться в любом месте программы с целью получения имени, использовавшегося для ее запуска (то есть первого аргумента командной строки). Первая из этих переменных, program_invocation_name, предоставляет полное путевое имя, использованное для запуска программы. Вторая переменная, program_invocation_short_name, обеспечивает версию этого имени без указания каких-либо каталогов (то есть базовую часть путевого имени). Объявления этих двух переменных могут быть получены из <errno.h> путем определения макроса _GNU_SOURCE.

Как показано на рис. 6.1, массивы argv и environ, а также строки, на которые они изначально указывают, находятся в одной непрерывной области памяти непосредственно над стеком процесса. (Массив environ, содержащий список переменных среды, будет рассмотрен в следующем разделе.) Предусмотрено верхнее ограничение общего количества байтов, сохраняемых в этой области. Согласно SUSv3 этот лимит можно определить через константу ARG_MAX (определенная в <limits.h>) или вызов sysconf(_SC_ARG_MAX). (Описание sysconf() дается в разделе 11.2.) В SUSv3 требуется, чтобы значение ARG_MAX было не меньше значения _POSIX_ARG_MAX, равного 4096 байт. Во многих реализациях UNIX допускается существенно более высокое ограничение. В SUSv3 не указано, входят ли в ограничение, определяемое ARG_MAX, служебные байты, характерные для реализации (завершающие нулевые байты, выравнивающие байты и массивы указателей argv и environ).

В Linux исторически сложилось так, что под ARG_MAX выделяется 32 страницы (то есть 131 072 байта в Linux/x86-32), включая пространство для служебных байтов. Начиная с версии ядра 2.6.23, ограничением на общее пространство, используемым для argv и environ, можно управлять через ограничение ресурса RLIMIT_STACK, и для argv и environ допускается гораздо большее значение. Это ограничение вычисляется как одна четвертая нежесткого ограничения ресурса RLIMIT_STACK, имевшего место на время вызова execve(). Дополнительные подробности можно найти на странице руководства execve(2).

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


6.7. Список переменных среды

У каждого процесса есть связанный с ним строковый массив, который называется списком переменных среды (окружения) или просто средой (окружением). Каждая из его строк является определением вида имя=значение. Таким образом, среда представляет собой набор пар «имя — значение», которые могут применяться для хранения произвольной информации.

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

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

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

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

export: $ SHELL=/bin/bash Создание переменной оболочки

$ export SHELL Помещение переменной в среду процесса оболочки

В оболочках bash и Korn можно воспользоваться следующей сокращенной записью:

$ export SHELL=/bin/bash

В оболочке C shell применяется альтернативная команда setenv:

% setenv SHELL /bin/bash

Показанные выше команды навсегда добавляют значение к среде оболочки, после чего эта среда наследуется всеми создаваемыми оболочкой дочерними процессами. Переменная среды может быть в любой момент удалена командой unset (unsetenv в оболочке C shell).

В оболочке Bourne shell и ее потомках (например, bash и Korn) для добавления значений к среде, применяемой для выполнения одной программы без влияния на родительскую оболочку (и последующие команды), может использоваться такой синтаксис:

$ NAME=value program /* ИМЯ=значение программа */

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

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

Текущий список переменных среды выводится на экран командой printenv. Вот как выглядит пример выводимой ею информации:

$ printenv

LOGNAME=mtk

SHELL=/bin/bash

HOME=/home/mtk

PATH=/usr/local/bin:/usr/bin:/bin:.

TERM=xterm

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

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

Список переменных среды любого процесса можно изучить, обратившись к характерному для Linux /proc/PID/environ, в котором каждая пара ИМЯ=значение заканчивается нулевым байтом.


Обращение к среде из программы

Из программы на языке C список переменных среды может быть доступен с помощью глобальной переменной char **environ. (Она определяется кодом инициализации среды выполнения программ на языке C, и ей присваивается значение, указывающее на местоположение списка переменных среды.) Как и argv, переменная environ указывает на список указателей на строки, заканчивающиеся нулевыми байтами, а сам этот список заканчивается значением NULL. Структура данных списка переменных среды в том же порядке, как их вывела выше команда printenv, показана на рис. 6.5.



Рис. 6.5. Пример структуры данных в отношении списка переменных среды для процесса


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


Листинг 6.3. Вывод на экран переменных среды процесса

proc/display_env.c

#include "tlpi_hdr.h"


extern char **environ;

int

main(int argc, char *argv[])

{

char **ep;

for (ep = environ; *ep!= NULL; ep++)

puts(*ep);

exit(EXIT_SUCCESS);

}

proc/display_env.c

Вывод программы совпадает с выводом команды printenv. Цикл в этой программе основан на использовании указателей для последовательного перебора содержимого массива environ. Даже если бы можно было рассматривать environ именно в качестве массива (как это делалось при использовании argv в листинге 6.2), это было бы менее естественно, поскольку элементы в списке переменных среды не располагаются в определенном порядке и нет переменной (соответствующей переменной argc), указывающей на размер списка переменных среды. (По той же причине элементы массива environ на рис. 6.5 не пронумерованы.)

Альтернативный метод обращения к списку переменных среды заключается в объявлении для функции main() третьего аргумента:

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

Этот аргумент может рассматриваться в том же качестве, что и environ, с той лишь разницей, что его область видимости является локальной для функции main(). Хотя это свойство широко реализовано в системах UNIX, его использования следует избегать, поскольку, вдобавок к ограничениям по области видимости, оно не указано в спецификации SUSv3.

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

#include <stdlib.h>


char *getenv(const char *name);

Возвращает указатель на строку (значение) или NULL, если такой переменной не существует

Получив имя переменной среды, функция getenv() возвращает указатель на соответствующее строковое значение. Если в ранее рассмотренном примере среды в качестве аргумента name указать SHELL, будет возвращена строка /bin/bash. Если переменной среды с таким именем не существует, getenv() возвращает NULL.

При использовании getenv() нужно учитывать следующие условия обеспечения портируемости.

• В SUSv3 требуется, чтобы приложение не изменяло строку, возвращенную getenv(). Дело в том, что в большинстве реализаций она является фактически частью среды (то есть строка, предоставляющая в паре ИМЯ=значение ту часть, которая является значением). Если нужно изменить значение переменной среды, можно воспользоваться одной из рассматриваемых далее функций: либо setenv(), либо putenv().

• В SUSv3 разрешается реализация функции getenv() для возвращения ее результата с использованием статически выделенного буфера, который может быть перезаписан последующими вызовами getenv(), setenv(), putenv() или unsetenv(). Хотя в glibc-реализации getenv() статический буфер таким образом не применяется, портируемая программа, которой нужно сохранить строку, возвращенную вызовом getenv(), прежде чем вызвать одну из этих функций, должна скопировать эту строку в другое место.


Изменение среды

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

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

Функция putenv() добавляет новую переменную к среде вызывающего ее процесса или изменяет значение существующей переменной.

#include <stdlib.h>


int putenv(char *string);

Возвращает 0 при успешном завершении или ненулевое значение при ошибке

Аргумент string является указателем на строку вида ИМЯ=значение. После вызова putenv() эта строка становится частью среды. Иначе говоря, строка, на которую указывает string, не будет скопирована в среду, а, наоборот, один из элементов среды будет указывать на то же самое место, что и string. Следовательно, если в дальнейшем изменить байты, на которые указывает string, такое изменение повлияет на среду процесса. Поэтому string не должна быть автоматически создаваемой переменной (то есть символьным массивом, размещаемым в стеке), поскольку эта область памяти может быть перезаписана после возвращения из функции, в которой определена переменная.

Обратите внимание на то, что putenv() возвращает при ошибке не –1, а ненулевое значение.

В glibc-реализации функции putenv() предоставляется нестандартное расширение. Если в строке, на которую указывает аргумент string, нет знака равенства (=), то переменная среды, идентифицируемая аргументом string, удаляется из списка переменных среды.

Функция setenv() является альтернативой putenv(), предназначенной для добавления переменной к среде.

#include <stdlib.h>


int setenv(const char *name, const char *value, int overwrite);

Возвращает 0 при успешном завершении или –1 при ошибке

Функция setenv() создает новую переменную среды путем выделения буфера памяти для строки вида ИМЯ=значение и копирует в этот буфер строки, указываемые аргументами name и value. Заметьте, что мы ни в коем случае не должны ставить знак равенства после name или в начале value, поскольку setenv() дописывает этот символ при добавлении нового определения к среде.

Функция setenv() не изменяет среду, если переменная, идентифицируемая аргументом name, уже существует, а аргумент overwrite имеет значение 0. Если у overwrite ненулевое значение, среда всегда изменяется.

Тот факт, что setenv() копирует свои аргументы, означает, что в отличие от putenv() мы можем впоследствии изменить содержимое строк, на которые указывают аргументы name и value, не оказывая влияния на среду. Это также означает, что использование автоматически создаваемых переменных в качестве аргументов setenv() не создает никаких проблем.

Функция unsetenv() удаляет из среды переменную, идентифицируемую аргументом name.

#include <stdlib.h>


int unsetenv(const char *name);

Возвращает 0 при успешном завершении или –1 при ошибке

Как и для setenv(), аргумент name не должен включать в себя знак равенства.

И setenv() и unsetenv() берут начало из BSD и не так популярны, как putenv(). Хотя в исходном стандарте POSIX.1 или в SUSv2 они не упоминались, их включили в SUSv3.

В версиях glibc, предшествующих 2.2.2, функция unsetenv() имела прототип, возвращающий void. Именно такой прототип unsetenv() был в исходной реализации BSD, и некоторые реализации UNIX до сих пор следуют этому BSD-прототипу.

Временами требуется удалить целиком всю среду, а затем выстроить ее заново с заданными значениями. Это, к примеру, может понадобиться для безопасного выполнения программ с установлением идентификатором пользователя (set-user-ID) (см. раздел 38.8). Среду можно удалить, присвоив переменной environ значение NULL:

environ = NULL;

Именно такое действие и предпринимается в библиотечной функции clearenv().

#define _BSD_SOURCE /* Или: #define _SVID_SOURCE */

#include <stdlib.h>


int clearenv(void)

Возвращает 0 при успешном завершении или ненулевое значение при ошибке

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

Функция clearenv() предоставляется во многих реализациях UNIX, но в SUSv3 она не определена. В SUSv3 определено, что, если приложение напрямую изменяет среду, как это делается функцией clearenv(), то поведение функций setenv(), unsetenv() и getenv() становится неопределенным. (Обоснование следующее: если запретить соответствующему приложению непосредствено изменять среду, то реализация ядра сможет полностью контролировать структуры данных, которые применяются ею для создания переменных среды.) Единственный способ очистки среды, разрешенный в SUSv3 приложению, заключается в получении списка всех переменных среды (путем извлечения их имен из environ), с последующим использованием функции unsetenv() для поименного удаления каждой переменной.


Пример программы

Использование всех ранее рассмотренных в этом разделе функций продемонстрировано в листинге 6.4. После начальной очистки среды программа добавляет любые определения среды, предоставленные в виде аргументов командной строки. Затем она добавляет определение для переменной GREET, если таковой еще не имеется в среде, удаляет определение для переменной BYE и, наконец, выводит на экран текущий список переменных среды. Вот как выглядит вывод этой программы:

$ ./modify_env "GREET=Guten Tag" SHELL=/bin/bash BYE=Ciao

GREET=Guten Tag

SHELL=/bin/bash

$ ./modify_env SHELL=/bin/sh BYE=byebye

SHELL=/bin/sh

GREET=Hello world

Если присвоить переменной environ значение NULL (как это делается при вызове clearenv() в листинге 6.4), то мы вправе ожидать, что следующий цикл (в том виде, в котором он используется в программе) даст сбой, поскольку запись *environ будет некорректна:

for (ep = environ; *ep!= NULL; ep++)

puts(*ep);

Но если функции setenv() и putenv() определят, что environ имеет значение NULL, они создадут новый список переменных среды и установят для environ значение, указывающее на этот список. Это приведет к тому, что описанный выше цикл станет работать правильно.


Листинг 6.4. Изменение среды процесса

proc/modify_env.c

#define _GNU_SOURCE /* Для получения различных объявлений из <stdlib.h> */

#include <stdlib.h>

#include "tlpi_hdr.h"


extern char **environ;


int

main(int argc, char *argv[])

{

int j;

char **ep;


clearenv(); /* Удаление всей среды */


for (j = 1; j < argc; j++)

if (putenv(argv[j])!= 0)

errExit("putenv: %s", argv[j]);

if (setenv("GREET", "Hello world", 0) == -1)

errExit("setenv");

unsetenv("BYE");

for (ep = environ; *ep!= NULL; ep++)

puts(*ep);

exit(EXIT_SUCCESS);

}

proc/modify_env.c


6.8. Выполнение нелокального перехода: setjmp() и longjmp()

Библиотечные функции setjmp() и longjmp() используются для нелокального перехода. Термин «нелокальный» обозначает, что цель перехода находится где-то за пределами той функции, которая выполняется в данный момент.

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

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

Рассмотрим следующий довольно распространенный сценарий, относящийся к обработке ошибки. В ходе глубоко вложенного вызова функции произошла ошибка, которая должна быть обработана за счет отказа от выполнения текущей задачи, возвращения сквозь несколько вызовов функций и последующего выполнения обработки в какой-нибудь функции более высокого уровня (возможно, даже main()). Добиться этого можно, если каждая функция станет возвращать код завершения, который будет проверяться и соответствующим образом обрабатываться вызывавшим ее кодом. Это вполне допустимый и во многих случаях желательный метод обработки такого рода развития событий. Но программирование давалось бы проще, если бы можно было перейти из середины вложенного вызова функции назад к одной из вызвавших ее функций (к непосредственно вызвавшей ее функции или к той функции, что вызвала эту функцию, и т. д.). Именно такая возможность и предоставляется функциями setjmp() и longjmp().

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

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

#include <setjmp.h>


int setjmp(jmp_buf env);

Возвращает 0 при первом вызове, ненулевое значение при возвращении через longjmp()

void longjmp(jmp_buf env, int val);

Вызов setjmp() устанавливает цель для последующего перехода, выполняемого функцией longjmp(). Этой целью является та самая точка в программе, откуда вызывается функция setjmp(). С точки зрения программирования после longjmp() это выглядит абсолютно так же, как будто мы только что вернулись из вызова setjmp() во второй раз. Способ, позволяющий отличить второе «возвращение» от первого, основан на целочисленном значении, возвращаемом функцией setjmp(). При первом вызове setjmp() возвращает 0, а при последующем, фиктивном возвращении предоставляется то значение, которое указано в аргументе val при вызове функции longjmp(). Путем использования для аргумента val различных значений можно отличать друг от друга переходы к одной и той же цели из различных мест программы.

Если бесконтрольно указать для функции longjmp() аргумент val, равный нулю, то это вызовет фиктивное возвращение из setjmp(), которое будет выглядеть, как будто это первое возвращение. По этой причине, если для val указано значение 0, longjmp() фактически применяет значение 1.

Используемый обеими функциями аргумент env предоставляет связующий элемент, позволяющий осуществить переход. При вызове setjmp() в env сохраняется различная информация о среде текущего процесса. Это позволяет выполнить вызов longjmp(), которому для осуществления фиктивного возвращения нужно указать ту же переменную env. Поскольку вызовы setjmp() и longjmp() — различные функции (в противном случае мы могли бы обойтись и простой инструкцией goto), env объявляется глобально или, что менее распространено, передается в качестве аргумента функции.

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

• Удалить из стека стековые фреймы для всех промежуточных функций между функцией, вызвавшей longjmp(), и функцией, которая перед этим вызвала setjmp(). Эту процедуру иногда называют «раскруткой стека», и она выполняется путем сброса регистра указателя стека и присвоением ему значения, сохраненного в аргументе env.

• Установить такое значение регистра, чтобы выполнение программы продолжалось с места предварительного вызова setjmp(). Это действие также выполняется с использованием значения, сохраненного в env.


Пример программы

Применение функций setjmp() и longjmp() показано в листинге 6.5. Эта программа с помощью предварительного вызова setjmp() устанавливает цель перехода. Дальнейшее использование инструкции switch (на основе значения, возвращаемого setjmp()) позволяет различить первоначальный возврат из функции setjmp() и возврат после longjmp(). Если возвращаемое значение равно 0, значит, только что был сделан первоначальный вызов setjmp() — мы вызываем функцию f1(), которая либо сразу же вызывает longjmp(), либо переходит к вызову f2(), в зависимости от значения argc (то есть количества аргументов командной строки). Если управление перешло в f2(), в ней тут же происходит вызов longjmp(). Вызов функции longjmp() из любой функции возвращает нас назад, к тому месту, из которого была вызвана setjmp(). В двух вызовах longjmp() используются разные аргументы val, поэтому инструкция switch в main() может определить функцию, из которой произошел переход, и вывести на экран соответствующее сообщение.

При запуске программы из листинга 6.5 без каких-либо аргументов командной строки мы увидим следующее:

$ ./longjmp

Вызов f1() после предварительного вызова setjmp()

Мы вернулись назад из f1()

Указание аргумента командной строки приводит к переходу, осуществляемому из f2():

$ ./longjmp x

Вызов f1() после предварительного вызова setjmp()

Мы вернулись назад из f2()


Листинг 6.5. Демонстрирует использование вызовов setjmp() и longjmp()

proc/longjmp.c

#include <setjmp.h>

#include "tlpi_hdr.h"


static jmp_buf env;

static void

f2(void)

{

longjmp(env, 2);

}


static void

f1(int argc)

{

if (argc == 1)

longjmp(env, 1);

f2();

}


int

main(int argc, char *argv[])

{

switch (setjmp(env)) {

case 0: /* Это возвращение после предварительного вызова setjmp() */

printf("Calling f1() after initial setjmp()\n");

f1(argc); /* Данная программа никогда не выполнит

break из следующей строки…. */

break; /*… но хороший тон обязывает нас его написать.*/


case 1:

printf("We jumped back from f1()\n");

break;


case 2:

printf("We jumped back from f2()\n");

break;

}


exit(EXIT_SUCCESS);

}

proc/longjmp.c


Ограничения, накладываемые на использование setjmp()

В SUSv3 и C99 указывается, что вызов setjmp() может присутствовать только в следующих контекстах:

• в качестве цельного управляющего выражения инструкции выбора или итерации (if, switch, while и т. д.);

• в качестве операнда унарного оператора! (НЕ), где получающееся в результате выражение является цельным управляющим выражением инструкции выбора или итерации;

• в качестве части операции сравнения (==,!=, < и т. д.), где другой операнд является выражением целочисленной константы и получающееся в результате выражение является цельным управляющим выражением инструкции выбора или итерации;

• в качестве обособленного вызова функции, не встроенного в какое-либо более сложное выражение.

Обратите внимание, что в приведенном выше списке отсутствует инструкция присваивания языка C. Инструкция, выраженная в следующей форме, не соответствует стандарту:

s = setjmp(env); /* НЕВЕРНО! */

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


Неверное применение longjmp()

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

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

2. Возвращение из функции x().

3. Вызов функции y(), выполняющей longjmp() с использованием env.

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

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


Проблемы, связанные с оптимизирующими компиляторами

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

Это говорит о том, что в результате вызова longjmp() в оптимизированных переменных могут оказаться неверные значения. Убедиться в этом можно, изучив поведение программы, представленной в листинге 6.6.


Листинг 6.6. Демонстрация взаимного влияния оптимизации при компиляции и функции longjmp()

proc/setjmp_vars.c

#include <stdio.h>

#include <stdlib.h>

#include <setjmp.h>


static jmp_buf env;

static void

doJump(int nvar, int rvar, int vvar)

{

printf("Inside doJump(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);

longjmp(env, 1);

}


int

main(int argc, char *argv[])

{

int nvar;

register int rvar; /* По возможности выделяется в регистре */

volatile int vvar; /* Смотрите текст */


nvar = 111;

rvar = 222;

vvar = 333;


if (setjmp(env) == 0) { /* Код, выполняемый после setjmp() */

nvar = 777;

rvar = 888;

vvar = 999;

doJump(nvar, rvar, vvar);

} else { /* Код, выполняемый после longjmp() */

printf("After longjmp(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);

}


exit(EXIT_SUCCESS);

}

proc/setjmp_vars.c

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

$ cc — o setjmp_vars setjmp_vars.c

$ ./setjmp_vars

Inside doJump(): nvar=777 rvar=888 vvar=999

After longjmp(): nvar=777 rvar=888 vvar=999

Но при компиляции с оптимизацией будут получены такие неожиданные результаты:

$ cc — O — o setjmp_vars setjmp_vars.c

$ ./setjmp_vars

Inside doJump(): nvar=777 rvar=888 vvar=999

After longjmp(): nvar=111 rvar=222 vvar=999

Здесь видно, что после вызова longjmp() переменные nvar и rvar были переопределены, получив значения, имевшиеся у них ко времени вызова функции setjmp(). Это произошло потому, что вследствие вызова longjmp() реорганизация оптимизатором кода привела к путанице. Эта проблема может коснуться любых локальных переменных, являющихся кандидатами на оптимизацию. Как правило, она касается переменных-указателей и переменных любого простого типа: char, int, float и long.

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

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

Если компилятору GNU C задать ключ — Wextra (extra warnings — «дополнительные предупреждения»), то в отношении программы setjmp_vars.c он выдаст следующие полезные предупреждения:

$ cc — Wall — Wextra — O — o setjmp_vars setjmp_vars.c

setjmp_vars.c: In function 'main':

setjmp_vars.c:17: warning: variable 'nvar' might be clobbered

by 'longjmp' or 'vfork'

(Переменная nvar может быть «затерта» функцией longjmp или vfork.)

setjmp_vars.c:18: warning: variable 'rvar' might be clobbered

by 'longjmp' or 'vfork'

(Переменная rvar может быть «затерта» функцией longjmp или vfork.)

Поучительно будет взглянуть на ассемблерный выход, создаваемый при компиляции программы setjmp_vars.c как с оптимизацией, так и без нее. Команда cc — S создает файл с расширением. s, где содержится сгенерированный для программы ассемблерный код.


Использовать ли функции setjmp() и longjmp()

Выше говорилось, что инструкции переходов goto могут создавать трудности при чтении программы. В свою очередь, нелокальные переходы могут на порядок затруднить чтение, поскольку способны передавать управление между двумя любыми функциями программы. Таким образом, использование функций setjmp() и longjmp() должно стать редким исключением. Лучше потратить дополнительные усилия в проектировании и написании кода, чтобы получить программу, в которой удастся обойтись без этих функций, и в результате она станет легче читаемой и, возможно, более портируемой. Мы еще будем рассматривать варианты этих функций (sigsetjmp() и siglongjmp(), описание которых дается в подразделе 21.2.1) при изучении сигналов, поскольку их иногда полезно применять при написании обработчиков сигналов.


6.9. Резюме

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

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

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

Аргументы командной строки, предоставляемые при запуске программы, становятся доступны через аргументы argc и argv функции main(). По соглашению в argv[0] содержится имя, использованное для вызова программы.

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

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


Дополнительная информация

Подробное описание системы управления виртуальной памятью можно найти в изданиях [Tanenbaum, 2007] и [Vahalia, 1996]. Алгоритмы управления памятью, используемые в ядре Linux, и соответствующий им код подробно рассмотрены в книге [Gorman, 2004].


6.10. Упражнения

6.1. Скомпилируйте программу из листинга 6.1 (mem_segments.c) и выведите на экран ее размер, воспользовавшись командой ls — l. Хотя программа содержит массив (mbuf), размер которого приблизительно составляет 10 Мбайт, размер исполняемого файла существенно меньше. Почему?

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

6.3. Реализуйте функции setenv() и unsetenv(), используя функции getenv(), putenv() и там, где это необходимо, код, который изменяет массив environ напрямую. Ваша версия функции unsetenv() должна проверять наличие нескольких определений переменной среды и удалять все определения (точно так же, как это делает glibc-версия функции unsetenv()).

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

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


7.1. Выделение памяти в куче

Процесс может выделить память, увеличив размер кучи (сегмента непрерывной виртуальной памяти переменного размера), который начинается сразу же после сегмента неинициализированных данных процесса и увеличивается/уменьшается по мере выделения/высвобождения памяти (см. рис. 6.1). Текущее ограничение кучи называется крайней точкой программы (program break).

Для выделения памяти в программах на языке C обычно используется семейство функций malloc, которое мы вскоре рассмотрим. Но сначала разберем функции brk() и sbrk(), на применении которых основана работа функций malloc.


7.1.1. Установка крайней точки программы: brk() и sbrk()

Изменение размеров кучи (то есть выделение и высвобождение памяти) сводится лишь к тому, чтобы всего лишь объяснить ядру, где располагается крайняя точка программы (program break). Изначально крайняя точка программы находится непосредственно сразу же за окончанием сегмента неинициализированных данных (то есть там же, где на рис. 6.1 стоит метка &end). После того как эта точка будет сдвинута вверх, программа сможет получать доступ к любому адресу во вновь выделенной области, но страницы физической памяти пока выделяться не будут. Ядро автоматически выделит новые физические страницы при первой же попытке процесса обратиться к адресам этих страниц.

Традиционно для манипуляций с крайней точкой программы система UNIX предоставляла два системных вызова, и они оба доступны в Linux: brk() и sbrk(). Хотя в программах эти системные вызовы напрямую используются довольно редко, в их работе стоит разобраться, чтобы выяснить порядок выделения памяти.

#include <unistd.h>


int brk(void *end_data_segment);

Возвращает 0 при успешном завершении или –1 при ошибке

void *sbrk(intptr_t increment);

Возвращает предыдущую крайнюю точку программы при успешном завершении или (void *) –1 при ошибке

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

Попытки установить крайнюю точку программы ниже ее первоначального значения, (то есть ниже метки &end), скорее всего, приведут к неожиданному поведению, например к сбою сегментирования (сигнал SIGSEGV рассматривается в разделе 20.2) при обращении к данным в уже не существующих частях сегментов инициализированных или неинициализированных данных. Точный верхний предел возможной установки крайней точки программы зависит от нескольких факторов, в числе которых: ограничение ресурсов процесса для размера сегмента данных (RLIMIT_DATA, рассматриваемое в разделе 36.3), а также расположение отображений памяти, сегментов совместно используемой памяти и совместно используемых библиотек.

Вызов sbrk() приводит к изменению положения точки программы путем добавления к ней приращения increment. (В Linux функция sbrk() является библиотечной и реализована в виде надстройки над функцией brk().) Используемый для описания приращения increment тип intptr_t является целочисленным типом данных. В случае успеха функция sbrk() возвращает предыдущий адрес крайней точки программы. Иными словами, если мы подняли крайнюю точку программы, то возвращаемым значением будет указатель на начало только что выделенного блока памяти.

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

В SUSv2 имеются описания brk() и sbrk() (с пометкой LEGACY, то есть устаревшие). Из SUSv3 эти описания удалены.


7.1.2. Выделение памяти в куче: malloc() и free()

Обычно в программах на языке C для выделения памяти в куче и ее высвобождения используется семейство функций malloc. Эти функции по сравнению с brk() и sbrk() предоставляют несколько преимуществ. В частности, они:

• стандартизированы в качестве части языка C;

• проще в использовании в программах, выполняемых в нескольких потоках;

• предоставляют простой интерфейс, позволяющий выделять память небольшими блоками;

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

Функция malloc() выделяет из кучи size байтов и возвращает указатель на начало только что выделенного блока памяти. Выделенная память не инициализируется.

#include <stdlib.h>


void *malloc(size_t size);

Возвращает при успешном завершении указатель на выделенную память или NULL при ошибке

Поскольку функция malloc() возвращает тип void *, ее можно присваивать любому типу указателя языка C. Блок памяти, возвращенный malloc(), всегда выравнивается по байтовой границе, обеспечиващей эффективное обращение к данным любого типа языка C. На практике это означает, что выделение на большинстве архитектур происходит по 8- или 16-байтовой границе.

В SUSv3 определяется, что вызов malloc(0) может возвращать либо NULL, либо указатель на небольшой фрагмент памяти, который может (и должен быть) высвобожден с помощью функции free(). В Linux вызов malloc(0) придерживается второго варианта поведения.

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

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

#include <stdlib.h>


void free(void *ptr);

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

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

• Это помогает свести к минимуму количество системных вызовов sbrk(), используемых программой. (Как уже отмечалось в разделе 3.1, системные вызовы приводят к небольшим, но все же существенным издержкам.)

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

Если аргумент, предоставляемый функции free(), является NULL-указателем, то при ее вызове ничего не происходит. (Иными словами, предоставление функции free() NULL-указателя не будет ошибкой.)

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


Пример программы

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

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


Листинг 7.1. Демонстрация происходящего с крайней точкой программы при высвобождении памяти

memalloc/free_and_sbrk.c

#define _BSD_SOURCE

#include "tlpi_hdr.h"

#define MAX_ALLOCS 1000000


int

main(int argc, char *argv[])

{

char *ptr[MAX_ALLOCS];

int freeStep, freeMin, freeMax, blockSize, numAllocs, j;


printf("\n");

if (argc < 3 || strcmp(argv[1], "-help") == 0)

usageErr("%s num-allocs block-size [step [min [max]]]\n", argv[0]);

numAllocs = getInt(argv[1], GN_GT_0, "num-allocs");

if (numAllocs > MAX_ALLOCS)

cmdLineErr("num-allocs > %d\n", MAX_ALLOCS);

blockSize = getInt(argv[2], GN_GT_0 | GN_ANY_BASE, "block-size");

freeStep = (argc > 3)? getInt(argv[3], GN_GT_0, "step"): 1;

freeMin = (argc > 4)? getInt(argv[4], GN_GT_0, "min"): 1;

freeMax = (argc > 5)? getInt(argv[5], GN_GT_0, "max"): numAllocs;


if (freeMax > numAllocs)

cmdLineErr("free-max > num-allocs\n");

printf("Initial program break: %10p\n", sbrk(0));

printf("Allocating %d*%d bytes\n", numAllocs, blockSize

for (j = 0; j < numAllocs; j++) {

ptr[j] = malloc(blockSize);

if (ptr[j] == NULL)

errExit("malloc");

}

printf("Program break is now: %10p\n", sbrk(0));

printf("Freeing blocks from %d to %d in steps of %d\n",

freeMin, freeMax, freeStep);

for (j = freeMin — 1; j < freeMax; j += freeStep)

free(ptr[j]);

printf("After free(), program break is: %10p\n", sbrk(0));


exit(EXIT_SUCCESS);

}

memalloc/free_and_sbrk.c

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

$ ./free_and_sbrk 1000 10240 2

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

Initial program break: 0x804a6bc

Allocating 1000*10240 bytes

Program break is now: 0x8a13000

Freeing blocks from 1 to 1000 in steps of 2

After free(), program break is: 0x8a13000

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

$ ./free_and_sbrk 1000 10240 1 1 999

Initial program break: 0x804a6bc

Allocating 1000*10240 bytes

Program break is now: 0x8a13000

Freeing blocks from 1 to 999 in steps of 1

After free(), program break is: 0x8a13000

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

$ ./free_and_sbrk 1000 10240 1 500 1000

Initial program break: 0x804a6bc

Allocating 1000*10240 bytes

Program break is now: 0x8a13000

Freeing blocks from 500 to 1000 in steps of 1

After free(), program break is: 0x852b000

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

Функция free() из библиотеки glibc осуществляет вызов sbrk() для снижения уровня крайней точки программы, только когда высвобождаемый блок на вершине кучи «достаточно» большой. Здесь «достаточность» определяется параметрами, которые управляют операциями пакета функции из семейства malloc (обычно это 128 Кбайт). Тем самым снижается количество необходимых вызовов sbrk() (то есть количество системных вызовов brk()).


Использовать или не использовать функцию free()

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

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

• Явный вызов функции free() может повысить читаемость и упростить сопровождение программы при необходимости ее доработок.

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


7.1.3. Реализация функций malloc() и free()

Хотя функциями malloc() и free() предоставляется интерфейс выделения памяти, который гораздо легче использовать, чем результат работы функций brk() и sbrk(), все же при его применении можно допустить ряд ошибок программирования. Разобраться в глубинных причинах таких ошибок и в способах их обхода поможет понимание внутреннего устройства функций malloc() и free().

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

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

Если посмотреть на реализацию функции free(), то там все организовано еще интереснее. Как free(), когда она помещает блок памяти в список свободных блоков, узнает, какого размера этот блок? Это делается благодаря особому приему. Когда функция malloc() выделяет блок, она выделяет дополнительные байты для хранения целочисленного значения, содержащего размер блока. Это значение находится в начале блока. Адрес, возвращаемый вызывавшему функцию коду, указывает на то место, которое следует сразу же за значением длины (рис. 7.1).



Рис. 7.1. Блок памяти, возвращенный функцией malloc()


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

Рис. 7.2. Блок в списке свободных блоков


По мере высвобождения и нового выделения памяти все блоки в списке свободных блоков станут перемежаться с выделенными, используемыми блоками памяти (рис. 7.3).



Рис. 7.3. Куча, содержащая выделенные блоки и блоки, входящие в список свободных блоков


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

Чтобы избежать подобных ошибок, нужно соблюдать следующие правила.

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

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

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

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


Средства и библиотеки для отладки выделения памяти

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

Среди всех средств отладки, предоставляемых библиотекой glibc, можно выделить следующие.

• Функции mtrace() и muntrace(), позволяющие программе включать и выключать отслеживание вызовов выделения памяти. Функции используются в сочетании с переменной среды MALLOC_TRACE — она должна быть определена для хранения имени файла, в который будет записываться трассировочная информация. После того как функция mtrace() будет вызвана, она проверит факт определения этого файла и возможность его открытия для записи. При положительном результате этой проверки все вызовы функций из пакета malloc будут отслеживаться и записываться в файл. Поскольку содержимое файла будет неудобочитаемым, для его анализа и создания читаемого результата предоставляется сценарий, который также называется mtrace. Из соображений безопасности вызовы mtrace() игнорируются программами с установленными идентификаторами пользователя и/или группы (set-group-ID).

• Функции mcheck() и mprobe() позволяют программе проверять корректность блоков выделенной памяти, например отлавливать такие ошибки, как попытки записи в те места, которые находятся за пределами блока выделенной памяти. Эти функции предоставляют возможности, которые несколько накладываются на возможности рассматриваемых далее библиотек отладки malloc. Программы, задействующие эти функции, должны быть скомпонованы с библиотекой mcheck с использованием ключа cc — lmcheck.

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

• 0 — означает игнорирование ошибок;

• 1 — устанавливает вывод диагностируемых ошибок на устройство стандартной ошибки — stderr;

• 2 — означает вызов функции abort() для прекращения выполнения программы.

Использование MALLOC_CHECK_ не позволяет обнаружить абсолютно все ошибки выделения и высвобождения памяти. С ее помощью можно найти только самые характерные из них. Тем не менее эта технология является быстродействующей и простой в использовании, а также имеет низкий уровень издержек в ходе выполнения программы по сравнению с применением библиотек отладки malloc. Из соображений безопасности установка значения для MALLOC_CHECK_ программами с полномочиями setuid и setgid игнорируется.

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

Библиотека отладки malloc предлагает такой же API, как и стандартный пакет malloc, но выполняет дополнительную работу по отлавливанию ошибок, допущенных при выделении памяти. Чтобы воспользоваться такой библиотекой, приложение следует скомпоновать вместе с ней, а не с пакетом malloc в стандартной библиотеке C. Поскольку использование таких библиотек обычно приводит к замедлению операций в ходе выполнения программы, увеличению потребления памяти или же и тому и другому вместе, их следует использовать только для отладки. После этого, при создании эксплуатационной версии приложения, нужно вернуться к компоновке со стандартным пакетом malloc. К таким библиотекам относятся Electric Fence (http://www.perens.com/FreeSoftware/), dmalloc (http://dmalloc.com/), Valgrind (http://valgrind.org/) и Insure++ (http://www.parasoft.com/).

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


Управление пакетом malloc и отслеживание его работы

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

• Функция mallopt() изменяет различные параметры, которые управляют алгоритмом, используемым функцией malloc(). Например, один из таких параметров определяет минимальный объем высвобождаемого пространства, которое должно быть в конце списка свободных блоков, перед тем как используется sbrk() для сжатия кучи. Еще один параметр указывает верхний предел для размера блоков, выделяемых из кучи. Блоки, превышающие этот предел, выделяются с использованием системного вызова mmap() (см. раздел 45.7).

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

Версии mallopt() и mallinfo() предоставляются многими реализациями UNIX. Но интерфейсы, предоставляемые этими функциями, у всех реализаций различаются, поэтому они не портируются.


7.1.4. Другие методы выделения памяти в куче

Наряду с malloc() библиотека языка C предоставляет ряд других функций для выделения памяти в куче. Они будут описаны в этом разделе.


Выделение памяти с помощью функций calloc() и realloc()

Функция calloc() выделяет память для массива одинаковых элементов.

#include <stdlib.h>


void *calloc(size_t numitems, size_t size);

Возвращает указатель на выделенную память при успешном завершении или NULL при ошибке

Аргумент numitems указывает количество выделяемых элементов, а аргумент size определяет их размер. После выделения блока памяти соответствующего размера calloc() возвращает указатель на начало блока (или NULL, если память не может быть выделена). В отличие от malloc(), функция calloc() инициализирует выделенную память нулевым значением.

Рассмотрим пример использования функции calloc():

struct myStruct { /* Определение нескольких полей */ };

struct myStruct *p;


p = calloc(1000, sizeof(struct myStruct));

if (p == NULL)

errExit("calloc");

Функция realloc() используется для изменения размера (обычно увеличения) блока памяти, ранее выделенного одной из функций из пакета malloc.

#include <stdlib.h>


void *realloc(void *ptr, size_t size);

Возвращает указатель на выделенную память при успешном завершении или NULL при ошибке

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

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

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

Память, выделенная с использованием функции calloc() или realloc(), должна быть высвобождена с помощью функции free().

Вызов realloc(ptr, 0) эквивалентен вызову free(ptr), за которым следует вызов malloc(0). Если для аргумента ptr указано значение NULL, то вызов функции realloc() становится эквивалентом вызова malloc(size).

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

Поскольку функция realloc() может изменить местоположение блока памяти, для последующих ссылок на этот блок нужно использовать возвращенное ею значение указателя. Функцию realloc() можно применять для перераспределения блока, на который указывает переменная ptr, следующим образом:

nptr = realloc(ptr, newsize);

if (nptr == NULL) {

/* Обработка ошибки */

} else { /* Выполнение realloc() завершилось успешно */

ptr = nptr;

}

В этом примере мы не стали присваивать возвращенное функцией realloc() значение непосредственно ptr. Если функция даст сбой, то для ptr установится значение NULL, что сделает существующий блок недоступным.

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


Выделение выровненной памяти: memalign() и posix_memalign()

Функции memalign() и posix_memalign() предназначены для выделения памяти, начиная с адреса, который будет кратен некоторой степени двойки, что может весьма пригодиться для отдельных приложений (см., к примеру, листинг 13.1).

#include <malloc.h>


void *memalign(size_t boundary, size_t size);

Возвращает указатель на выделенную память при успешном завершении или NULL при ошибке

Функция memalign() выделяет size байтов, начиная с адреса, выровненного по границе, кратной степени числа два. В результате выполнения функция возвращает адрес выделенной памяти.

Функция memalign() присутствует не во всех реализациях UNIX. Большинство других реализаций UNIX, предоставляющих memalign(), для получения объявления функции требуют включения вместо <malloc.h> заголовочного файла <stdlib.h>.

В SUSv3 функция memalign() не указана, но вместо нее есть точно такая же функция под названием posix_memalign(). Эта функция была недавно создана комитетом по стандартизации и появилась лишь в нескольких реализациях UNIX.

#include <stdlib.h>


int posix_memalign(void **memptr, size_t alignment, size_t size);

Возвращает 0 при успешном завершении или номер ошибки в виде положительного числа при ошибке

Функция posix_memalign() отличается от memalign() двумя деталями:

• адрес выделенной памяти возвращается в memptr;

• память выравнивается по значению степени числа два, которое кратно значению sizeof(void *) (4 или 8 байт в большинстве аппаратных архитектур).

Обратите внимание также на необычное возвращаемое этой функцией значение. Вместо того чтобы при ошибке возвратить –1, она возвращает номер ошибки (то есть положительное целое число того типа, который обычно возвращается в errno).

Если значение sizeof(void *) равно 4, то так с помощью функции posix_memalign() можно выделить 65 536 байт памяти, выровненных по 4096-байтовой границе:

int s;

void *memptr;


s = posix_memalign(&memptr, 1024 * sizeof(void *), 65536);

if (s!= 0)

/* Обработка ошибки */

Блоки памяти, выделенные с использованием memalign() или posix_memalign(), должны высвобождаться с помощью функции free().

В некоторых реализациях UNIX невозможно вызвать функцию free() в отношении блока памяти, выделенного с помощью memalign(), поскольку в реализации memalign() для выделения блока памяти используется функция malloc(), а затем возвращается указатель на адрес с соответствующем выравниванием в этом блоке. Реализация функции memalign() в библиотеке glibc от этого ограничения не страдает.


7.2. Выделение памяти в стеке: alloca()

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

#include <alloca.h>


void *alloca(size_t size);

Возвращает указатель на выделенный блок памяти

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

Нам не нужно, а на самом деле мы не должны вызывать функцию free() для высвобождения памяти, выделенной с помощью функции alloca(). Точно так же невозможно использовать функцию realloc() для изменения блока памяти, выделенного с помощью alloca().

Хотя функция alloca() не является частью SUSv3, она предоставляется большинством реализаций UNIX, и поэтому ее можно считать достаточно портируемой.

В старых версиях glibc и в некоторых других реализациях UNIX (главным образом производных от BSD) для получения объявления функции alloca() требуется включение вместо <alloca.h> заголовочного файла <stdlib.h>.

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

Учтите, что alloca() нельзя использовать внутри списка аргументов функции, как в следующем примере:

func(x, alloca(size), z); /* НЕВЕРНО! */

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

void *y;


y = alloca(size);

func(x, y, z);

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

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

Функция alloca() может особенно пригодиться при использовании функции longjmp() (см. раздел 6.8) или siglongjmp() (см. подраздел 21.2.1) для выполнения нелокального перехода из обработчика сигнала. В этом случае очень трудно или даже невозможно избежать утечки памяти, если она выделяется для той функции, над которой осуществляется переход, с помощью функции malloc(). Для сравнения, функция alloca() позволяет полностью избежать подобной проблемы, поскольку, как только стек будет отмотан этими вызовами назад, выделенная память автоматически высвободится.


7.3. Резюме

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

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


7.4. Упражнения

7.1. Измените программу из листинга 7.1 (free_and_sbrk.c) так, чтобы она выводила текущее значение крайней точки программы после каждого выполнения функции malloc(). Запустите программу, указав небольшой размер выделяемого блока. Тем самым будет продемонстрировано, что функция malloc() не использует sbrk() для изменения положения крайней точки программы при каждом вызове, а вместо этого периодически выделяет более крупные фрагменты памяти, из которых возвращает вызывающему коду небольшие фрагменты.

7.2. (Повышенной сложности.) Реализуйте функции malloc() и free().

8. Пользователи и группы

У каждого пользователя имеется уникальное имя для входа в систему и связанный с ним числовой идентификатор пользователя (UID). Пользователи могут состоять в одной или нескольких группах. У каждой группы также есть уникальное имя и идентификатор группы (GID).

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

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


8.1. Файл паролей: /etc/passwd

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

mtk: x:1000:100:Michael Kerrisk:/home/mtk:/bin/bash

Рассмотрим эти поля по порядку следования.

• Имя для входа в систему. Это уникальное имя, которое пользователь должен вводить при входе в систему. Зачастую его также называют именем пользователя. Имя для входа в систему можно рассматривать как легко читаемый (символьный) идентификатор, соответствующий числовому идентификатору пользователя (который вскоре будет рассмотрен). Это имя (вместо числового UID) выводят на экран при запросе принадлежности файла такие программы, как ls(1), например при вводе команды ls — l.

Зашифрованный пароль. В этом поле содержится 13-символьный зашифрованный пароль (более подробно мы рассмотрим его в разделе 8.5). Если в поле пароля содержится любая другая строка, в частности строка с другим количеством символов, значит, вход с этой учетной записью недопустим, поскольку такая строка не может представлять действующий зашифрованный пароль. При этом следует учесть, что, если включен режим теневых паролей (что обычно и бывает), данное поле игнорируется. В этом случае поле пароля в /etc/passwd содержит букву x (хотя на ее месте может быть любая непустая символьная строка), а зашифрованный пароль хранится в теневом файле (см. раздел 8.2). Если поле пароля в /etc/passwd пустое, значит, для регистрации под этой учетной записью пароль не нужен (это правило действует даже при наличии теневых паролей).

Здесь будет считаться, что пароли зашифрованы с помощью исторически сложившейся и по-прежнему широко используемой в UNIX схемы шифрования паролей под названием Data Encryption Standard (DES). Схему DES можно заменить другими схемами, например MD5, которая создает из данных на входе 128-битный профиль сообщения (разновидность хеша). В файле паролей (или теневом файле паролей) это значение сохраняется в виде 34-символьной строки.

Идентификатор пользователя (UID). Это числовой идентификатор данного пользователя. Если поле хранит значение 0, то пользователь с данной учетной записью — привилегированный (суперпользователь). Как правило, имеется только одна такая учетная запись, у которой в качестве имени для входа в систему используется слово root. В Linux 2.2 и более ранних версиях идентификаторы пользователей хранились в виде 16-битных значений, позволяющих иметь UID в диапазоне от 0 до 65 535. В Linux 2.4 и более поздних версиях идентификаторы хранятся с использованием 32 бит, позволяя задействовать значительно более широкий диапазон.

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

Идентификатор группы (GID). Это числовой идентификатор первой из групп, в которую входит пользователь. Дальнейшая принадлежность к группам этого пользователя определена в системном файле групп.

Комментарий. Это поле содержит текст, описывающий пользователя. Такой текст выводится различными программами, например finger(1).

Домашний каталог. Исходный каталог, в который пользователь попадает после входа в систему. Содержимое этого поля становится значением переменной среды HOME.

• Оболочка входа в систему. Это программа, которой передается управление после входа пользователя в систему. Обычно это одна из оболочек, например bash, но может быть и любая другая программа. Если это поле остается пустым, то в качестве исходной применяется оболочка /bin/sh, Bourne shell. Содержимое поля становится значением переменной среды SHELL.

В автономной системе вся информация, касающаяся паролей, находится в файле /etc/passwd. Но если для хранения паролей в сетевой среде используется такая система, как Network Information System (NIS) или Lightweight Directory Access Protocol (LDAP), часть этой информации или же вся она целиком находится в удаленной системе. Поскольку программы, обращающиеся за информацией о паролях, используют рассматриваемые далее функции (getpwnam(), getpwuid() и т. д.), приложениям безразлично, что именно применяется: NIS или LDAP. То же самое можно сказать и о теневых файлах паролей и групп, рассматриваемых в следующих разделах.


8.2. Теневой файл паролей: /etc/shadow

Исторически сложилось так, что в системах UNIX вся информация о пользователях, включая зашифрованные пароли, хранится в файле /etc/passwd. В связи с этим возникают проблемы безопасности. Поскольку различным непривилегированным системным утилитам требуется доступ для чтения к другой информации, содержащейся в файле паролей, ее нужно делать доступной для чтения для всех пользователей. Тем самым открывается лазейка для программ по взлому паролей, пытающихся их расшифровать с помощью длинных списков наиболее вероятных вариантов (например, стандартных записей из словарей или имен людей), чтобы определить, соответствуют ли они зашифрованному паролю пользователя. Теневой файл паролей, /etc/shadow, был разработан как средство противостояния таким атакам. Замысел заключается в том, что вся неконфиденциальная информация о пользователе находится в открытом, доступном для чтения файле паролей, а зашифрованные пароли хранятся в теневом файле паролей, доступном для чтения только программам с особыми привилегиями.

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

Теневые пароли в SUSv3 не определены. Кроме того, они предоставляются не всеми реализациями UNIX.


8.3. Файл групп: /etc/group

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

Набор групп, к которым принадлежит пользователь, определен в виде сочетания поля идентификатора группы в записи пользователя в файле паролей и групп, под которыми этот пользователь перечисляется в файле групп. Это странное разбиение информации на два файла сложилось исторически. В ранних реализациях UNIX можно было одновременно входить только в одну группу. Исходная группа, в которую входил пользователь при входе в систему, определялась полем GID файла паролей и могла быть в нем изменена после использования команды newgrp(1). Эта команда требовала от пользователя предоставить пароль группы (если вход в группу был защищен паролем). В 4.2BSD было введено понятие одновременной принадлежности к нескольким группам, позже ставшее стандартом в POSIX.1-1990. Согласно этой схеме в файле групп имелся список принадлежности каждого пользователя к дополнительным группам. (Команда groups(1) выводит либо те группы, в которые входит данный процесс оболочки, либо, если были переданы (одно или несколько) имена пользователей, — те группы, в которые входят эти пользователи.)

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

users: x:100:

jambit: x:106:claus,felli,frank,harti,markus,martin,mtk,paul

Рассмотрим эти поля в порядке следования.

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

Зашифрованный пароль. Это поле содержит необязательный пароль группы. С появлением возможности принадлежать сразу нескольким группам в наши дни в системах UNIX пароли групп используются крайне редко. Тем не менее в это поле можно поместить пароль группы (привилегированный пользователь может сделать это с помощью команды gpasswd). Если пользователь не входит в группу, newgrp(1) запрашивает этот пароль перед запуском новой оболочки. Если включены теневые пароли, это поле игнорируется (в этом случае по соглашению в нем содержится только буква x, но вместо нее может указываться любая строка, включая пустую), а зашифрованный пароль в действительности хранится в теневом файле групп, /etc/gshadow, доступ к которому могут получить только привилегированные пользователи или программы. Пароли групп шифруются точно таким же образом, что и пароли пользователей (см. раздел 8.5).

Идентификатор группы (GID). Это числовой идентификатор для данной группы. Как правило, есть группа, имеющая в качестве идентификатора число 0, — это группа с названием root (так же как и запись в /etc/passwd с пользовательским идентификатором со значением 0). В Linux 2.2 и более ранних версиях идентификаторы групп хранились в виде 16-битных значений, позволяющих иметь ID в диапазоне от 0 до 65 535. В Linux 2.4 и более поздних версиях идентификаторы хранятся с использованием 32 бит.

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

Следующая запись в файле паролей означает, что пользователь avr входит в группы users, staff и teach:

avr: x:1001:100:Anthony Robins:/home/avr:/bin/bash

А в файле групп будут такие записи:

users: x:100:

staff: x:101:mtk,avr,martinl

teach: x:104:avr,rlb,alc

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


8.4. Извлечение информации о пользователях и группах

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


Извлечение записей из файла паролей

Извлечение записей из файла паролей проводится с помощью функций getpwnam() и getpwuid().

#include <pwd.h>


struct passwd *getpwnam(const char *name);

struct passwd *getpwuid(uid_t uid);

Обе функции при успешном завершении возвращают указатель, при ошибке — NULL. Описание для случая «запись не найдена» дается в тексте подраздела

При предоставлении имени в качестве аргумента name функция getpwnam() возвращает указатель на структуру следующего типа, содержащую соответствующую информацию из записи в файле паролей:

struct passwd {

char *pw_name; /* Имя для входа в систему (имя пользователя) */

char *pw_passwd; /* Зашифрованный пароль */

uid_t pw_uid; /* Идентификатор пользователя */

gid_t pw_gid; /* Идентификатор группы */

char *pw_gecos; /* Комментарий (информация о пользователе) */

char *pw_dir; /* Исходный рабочий (домашний) каталог */

char *pw_shell; /* Оболочка входа в систему */

};

Поля pw_gecos и pw_passwd структуры passwd в SUSv3 не определены, но доступны во всех реализациях UNIX. Поле pw_passwd содержит актуальную информацию только при выключенном режиме использования теневых паролей. (С точки зрения программирования наипростейший способ выявить включение режима использования теневых паролей состоит в вызове функции getspnam() (вскоре рассмотрим) сразу же после успешного выполнения функции getpwnam(), чтобы увидеть, сможет ли она возвратить запись теневого пароля для того же имени пользователя.) В некоторых других реализациях в этой структуре предоставляются дополнительные нестандартные поля.

Поле pw_gecos происходит из ранних реализаций UNIX, где в нем содержалась информация для связи с машиной, на которой запущена операционная система General Electric Comprehensive Operating System (GECOS). Хотя эта цель его применения давно устарела, имя поля осталось прежним, а само оно предназначено для записи информации о пользователе.

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

Поскольку функции getpwnam() и getpwuid() возвращают указатель на статически выделенную структуру, они являются нереентерабельными. На самом деле ситуация складывается еще сложнее, поскольку возвращаемая структура passwd содержит указатели на другую информацию (например, поле pw_name), которая также является статически выделенной. (Реентерабельность объясняется в подразделе 21.1.2.) Такие же утверждения справедливы для функций getgrnam() и getgrgid(), которые мы вскоре рассмотрим.

В SUSv3 указывается эквивалентный набор реентерабельных функций — getpwnam_r(), getpwuid_r(), getgrnam_r() и getgrgid_r(), включающих в качестве аргументов как структуру passwd (или group), так и область буфера для хранения других структур, на которые указывают поля структуры passwd (group). Количество байтов, требуемое для этого дополнительного буфера, может быть получено с помощью вызова sysconf(_SC_GETPW_R_SIZE_MAX) (или sysconf(_SC_GETGR_R_SIZE_MAX) для функций, имеющих отношение к группам). Дополнительные сведения об этих функциях можно найти на страницах руководства.

В соответствии с положениями SUSv3, если нужная запись passwd не может быть найдена, функции getpwnam() и getpwuid() должны возвратить значение NULL, оставив значение errno в неизменном виде. Таким образом, можно различить ошибку и случаи «запись не найдена», используя следующий код:

struct passwd *pwd;


errno = 0;

pwd = getpwnam(name);

if (pwd == NULL) {

if (errno == 0)

/* Запись не найдена */;

else

/* Ошибка */;

}

Однако некоторые реализации UNIX не соответствуют [требованиям] SUSv3 по этому вопросу. Если нужная запись passwd не найдена, эти функции возвращают значение NULL и устанавливают для errno ненулевое значение, например ENOENT или ESRCH. До выхода версии 2.7 библиотека glibc выдавала в таком случае ошибку ENOENT, но, начиная с версии 2.7, она стала отвечать требованиям SUSv3. Эти расхождения в реализациях возникли отчасти из-за того, что в POSIX.1-1990 данным функциям не требовалось устанавливать для errno значения при ошибке и позволялось устанавливать значение в случае «запись не найдена». В результате при использовании этих функций стало совершенно невозможно портируемым образом отличить ошибку от ситуации «запись не найдена».


Извлечение записей из файла групп

Записи из файла групп извлекаются с помощью функций getgrnam() и getgrgid().

#include <grp.h>


struct group *getgrnam(const char *name);

struct group *getgrgid(gid_t gid);

Обе функции при успешном завершении возвращают указатель, при ошибке — NULL. Описание для случая «запись не найдена» дается в тексте подраздела

Функция getgrnam() осуществляет поиск информации о группе по имени группы, а функция getgrgid() — по идентификатору группы. Обе функции возвращают указатель на структуру следующего типа:

struct group {

char *gr_name; /* Имя группы */

char *gr_passwd; /* Зашифрованный пароль (в режиме без теневых паролей) */

gid_t gr_gid; /* Идентификатор группы */

char **gr_mem; /* Массив указателей на имена участников группы,

перечисленных в /etc/group, завершающийся значением NULL */

};

Поле gr_passwd структуры group в SUSv3 не указано, но доступно в большинстве реализаций UNIX.

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

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


Пример программы

Один из примеров наиболее частого применения рассмотренных в этом разделе функций — преобразование символьных имен пользователя и группы в их числовые идентификаторы и наоборот. В листинге 8.1 показано это преобразование в виде четырех функций: userNameFromId(), userIdFromName(), groupNameFromId() и groupIdFromName(). Для удобства вызывающего функции userIdFromName() и groupIdFromName() также позволяют аргументу name быть числовой строкой в чистом виде. В этом случае строка преобразуется непосредственно в число и возвращается вызвавшему функцию коду. Эти функции будут использоваться в некоторых примерах программ, которые мы рассмотрим далее в книге.


Листинг 8.1. Функции для преобразования идентификаторов пользователей и групп в имена пользователей и групп и наоборот

users_groups/ugid_functions.c

#include <pwd.h>

#include <grp.h>

#include <ctype.h>

#include "ugid_functions.h" /* Объявление определяемых здесь функций */


char * /* Возвращает имя, соответствующее 'uid', или NULL при ошибке */

userNameFromId(uid_t uid)

{

struct passwd *pwd;


pwd = getpwuid(uid);

return (pwd == NULL)? NULL: pwd->pw_name;

}


uid_t /* Возвращает идентификатор пользователя,

соответствующего 'name', или –1 при ошибке */

userIdFromName(const char *name)

{

struct passwd *pwd;

uid_t u;

char *endptr;

if (name == NULL || *name == '\0') /* Возвращает ошибку, если передан NULL*/

return -1; /* или пустая строка */


u = strtol(name, &endptr, 10); /* Для удобства вызывающего */

if (*endptr == '\0') /* разрешение числовой строки */

return u;

pwd = getpwnam(name);

if (pwd == NULL)

return -1;


return pwd->pw_uid;

}


char * /* Возвращает имя, соответствующее 'gid', или NULL при ошибке */

groupNameFromId(gid_t gid)

{

struct group *grp;


grp = getgrgid(gid);

return (grp == NULL)? NULL: grp->gr_name;

}

gid_t /* Возвращает идентификатор группы, */

/* соответствующего 'name',или -1 при ошибке */

groupIdFromName(const char *name)

{

struct group *grp;

gid_t g;

char *endptr;


if (name == NULL || *name == '\0') /* Возвращает ошибку, если передан NULL*/

return -1; /* или пустая строка */


g = strtol(name, &endptr, 10); /* Для удобства вызывающего */

if (*endptr == '\0') /* разрешение числовой строки */

return g;


grp = getgrnam(name);

if (grp == NULL)

return -1;


return grp->gr_gid;

}

users_groups/ugid_functions.c


Сканирование всех записей в файлах паролей и групп

Функции setpwent(), getpwent() и endpwent() используются для выполнения последовательного сканирования записей в файле паролей.

#include <pwd.h>


struct passwd *getpwent(void);

Возвращает указатель при успешном завершении или NULL в случае конца потока или при ошибке

void setpwent(void);

void endpwent(void);

Функция getpwent() поочередно возвращает записи из файла паролей, выдавая NULL, когда записей уже больше нет (или при возникновении ошибки). При первом вызове функция автоматически открывает файл паролей. Когда работа с файлом завершена, для его закрытия вызывается функция endpwent().

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

struct passwd *pwd;


while ((pwd = getpwent())!= NULL)

printf("%-8s %5ld\n", pwd->pw_name, (long) pwd->pw_uid);


endpwent();

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

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


Извлечение записей из теневого файла паролей

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

#include <shadow.h>


struct spwd *getspnam(const char *name);

Возвращает при успешном завершении указатель или NULL, если запись не найдена либо произошла ошибка

struct spwd *getspent(void);

Возвращает указатель при успешном завершении или NULL в случае конца потока либо при ошибке

void setspent(void);

void endspent(void);

Мы не станем рассматривать эти функции во всех подробностях, поскольку их работа похожа на работу соответствующих функций, относящихся к файлу паролей. (Эти функции не указаны в SUSv3 и представлены не во всех реализациях UNIX.)

Функции getspnam() и getspent() возвращают указатели на структуру типа spwd. Она имеет следующую форму:

struct spwd {

char *sp_namp; /* Имя для входа в систему (имя пользователя) */

char *sp_pwdp; /* Зашифрованный пароль */


/* Остальные поля поддерживают «устаревание пароля», дополнительное средство,

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

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

бесполезным. */


long sp_lstchg; /* Время последнего изменения пароля (количество

дней, прошедших с 1 января 1970 года) */

long sp_min; /* Минимальное количество дней между сменами пароля */

long sp_max; /* Максимальное количество дней до требуемой смены пароля */

long sp_warn; /* Количество дней, за которое пользователь

заранее получает предупреждение о скором

истечении срока действия пароля */

long sp_inact; /* Количество дней после истечения срока действия пароля

до признания учетной записи неактивнойи заблокированной */

long sp_expire; /* Дата, когда истекает срок действия учетной

записи (количество дней, прошедших с 1 января 1970 года) */

unsigned long sp_flag; /* Зарезервировано для будущего использования */

};

Применение функции getspnam() будет показано в листинге 8.2.


8.5. Шифрование пароля и аутентификация пользователя

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

Из соображений безопасности системы UNIX шифруют пароли, используя алгоритм одностороннего шифрования. Он гарантирует невозможность воссоздания исходного пароля из его зашифрованной формы. Поэтому единственный способ проверить верность проверяемого пароля — его шифрование с использованием того же метода, что позволит увидеть, соответствует ли зашифрованный результат значению, сохраненному в файле /etc/shadow. Алгоритм шифрования заключен в функции crypt().

#define _XOPEN_SOURCE

#include <unistd.h>


char *crypt(const char *key, const char *salt);

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

Работа функции crypt() предусматривает получение ключа key (то есть пароля) длиной до восьми символов и применение к нему разновидности алгоритма Data Encryption Standard (DES). Аргумент salt является строкой из двух символов, чье значение используется для внесения помех в алгоритм (его изменения), то есть для применения технологии, затрудняющей взлом зашифрованного пароля. Функция возвращает указатель на статически выделенную 13-символьную строку, являющуюся зашифрованным паролем.

Подробности, касающиеся алгоритма DES, можно найти по адресу http://www.itl.nist.gov/fipspubs/fip46-2.htm. Как уже ранее упоминалось, вместо DES могут использоваться другие алгоритмы. Например, применение алгоритма MD5 приводит к созданию 34-символьной строки, начинающейся с символа доллара ($), который позволяет функции crypt() отличать пароли, зашифрованные с помощью DES, от паролей, зашифрованных с помощью MD5.

При рассмотрении вопроса шифрования паролей здесь употребляется слово «шифрование», что не совсем верно отражает действительность. Если выражаться точнее, то DES использует заданную строку пароля в качестве ключа шифрования для зашифровки фиксированной строки битов, а MD5 представляет собой сложный тип функции хеширования. Результат в обоих случаях получается один и тот же: не поддающееся расшифровке и необратимое преобразование входного пароля.

И аргумент salt, и шифруемый пароль состоят из символов, выбранных из 64-символьного набора [a-zA-Z0-9/.]. Таким образом, аргумент salt («соль»), состоящий из двух символов, может стать причиной изменения алгоритма шифрования любым из 64 × 64 = 4096 возможных способов. Это означает, что вместо предварительного шифрования целого словаря и проверки зашифрованного пароля на совпадение со всеми словами в словаре взломщику придется проверять пароль на соответствие 4096 зашифрованным версиям словарей.

Зашифрованный пароль, возвращенный функцией crypt(), содержит в двух первых символах копию исходного значения «соли». Это означает, что при шифровании потенциально подходящего пароля можно получить соответствующее значение «соли» из значения зашифрованного пароля, уже хранящегося в файле /etc/shadow. (Такие программы, как passwd(1), при шифровании нового пароля создают произвольное значение «соли».) Фактически функция crypt() игнорирует любые символы в строке «соли», кроме первых двух. Поэтому можно указать в качестве аргумента salt сам зашифрованный пароль.

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


Пример программы

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

#define _BSD_SOURCE

#include <unistd.h>


char *getpass(const char *prompt);

Возвращает при успешном завершении указатель на статически размещаемую строку ввода пароля или NULL при ошибке

Функция getpass() сначала отключает отображение на экране и всю обработку специальных символов управления терминалом (таких как символ прерывания, обычно это Ctrl+C). (Способы изменения этих настроек терминала рассматриваются в главе 58.) Затем на экран выводится строка с приглашением на ввод и считывается введенная строка, а в качестве результата выполнения функции возвращается строка ввода с завершающим нулевым байтом и удаленным следующим за ней символом новой строки. (Эта строка размещается статически и поэтому будет перезаписана при следующем вызове getpass().) Перед возвращением getpass() восстанавливает настройки терминала до их исходного состояния.

Прочитав пароль с помощью функции getpass(), программа из листинга 8.2 проверяет его. При этом функция crypt() используется для его шифрования и проверки того, что получившаяся строка в точности совпадает зашифрованному паролю, записанному в теневом файле паролей. Если пароль совпадает, идентификатор пользователя выводится на экран, как в следующем примере:

$ su Для чтения теневого файла паролей нужны привилегии

Password:

# ./check_password

Username: mtk

Password: Набирается пароль, который не отображается на экране

Successfully authenticated: UID=1000

Программа в листинге 8.2 определяет размер массива символов, содержащего имя пользователя. Для этого применяется значение, возвращенное выражением sysconf(_SC_LOGIN_NAME_MAX), которое выдает максимальный размер имени пользователя в главной системе. Использование sysconf() объясняется в разделе 11.2.


Листинг 8.2. Аутентификация пользователя с применением теневого файла паролей

users_groups/check_password.c

#define _BSD_SOURCE /* Получение объявления getpass() из <unistd.h> */

#define _XOPEN_SOURCE /* Получение объявления crypt() из <unistd.h> */

#include <unistd.h>

#include <limits.h>

#include <pwd.h>

#include <shadow.h>

#include "tlpi_hdr.h"


int

main(int argc, char *argv[])

{

char *username, *password, *encrypted, *p;

struct passwd *pwd;

struct spwd *spwd;

Boolean authOk;

size_t len;

long lnmax;


lnmax = sysconf(_SC_LOGIN_NAME_MAX);

if (lnmax == -1) /* Если предел не определен, */

lnmax = 256; /* выбираем наугад */


username = malloc(lnmax);

if (username == NULL)

errExit("malloc");

printf("Username: ");

fflush(stdout);

if (fgets(username, lnmax, stdin) == NULL)

exit(EXIT_FAILURE); /* Выход при встрече EOF */


len = strlen(username);

if (username[len — 1] == '\n')

username[len — 1] = '\0'; /* Удаление завершающего '\n' */


pwd = getpwnam(username);

if (pwd == NULL)

fatal("couldn't get password record");

spwd = getspnam(username);

if (spwd == NULL && errno == EACCES)

fatal("no permission to read shadow password file");


if (spwd!= NULL) /* Если есть запись теневого пароля */

pwd->pw_passwd = spwd->sp_pwdp; /* Использование теневого пароля */

password = getpass("Password: ");


/* Шифрование пароля с немедленным уничтожением незашифрованной версии */


encrypted = crypt(password, pwd->pw_passwd);

for (p = password; *p!= '\0';)

*p++ = '\0';


if (encrypted == NULL)

errExit("crypt");


authOk = strcmp(encrypted, pwd->pw_passwd) == 0;

if (!authOk) {

printf("Incorrect password\n");

exit(EXIT_FAILURE);

}


printf("Successfully authenticated: UID=%ld\n", (long) pwd->pw_uid);


/* Здесь совершаем то, ради чего аутентифицировались… */


exit(EXIT_SUCCESS);

}

users_groups/check_password.c

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

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

Функция getpass() фигурировала в SUSv2 с пометкой LEGACY (устаревшая), где отмечалось, что ее название вводит в заблуждение и она предоставляет функциональные возможности, которые в любом случае можно легко реализовать. Из SUSv3 спецификация getpass() была удалена. Тем не менее она встречается во многих реализациях UNIX.


8.6. Резюме

У каждого пользователя есть уникальное имя для входа в систему и связанный с ним числовой идентификатор. Пользователи могут принадлежать одной или нескольким группам, у каждой из которых также есть уникальное имя и связанный с ним числовой ID. Основная цель этих идентификаторов — доказательство факта принадлежности различных системных ресурсов (например, файлов) к группам и полномочий на доступ к ним.

Имя пользователя и идентификатор определяются в файле /etc/passwd, который содержит и другую информацию о пользователе. Принадлежность пользователя к той или иной группе определяется полями в файлах /etc/passwd и /etc/group. Еще один файл, /etc/shadow, может быть прочитан только привилегированными процессами. Он применяется для отделения конфиденциальной парольной информации от пользовательских сведений, находящихся в открытом доступе в файле /etc/passwd. Для извлечения информации из каждого из этих файлов предоставляются различные библиотечные функции.

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


8.7. Упражнения

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

printf("%s %s\n", getpwuid(uid1)->pw_name,

getpwuid(uid2)->pw_name);

8.2. Реализуйте функцию getpwnam(), используя функции setpwent(), getpwent() и endpwent().

9. Идентификаторы процессов

У каждого процесса есть набор связанных с ним числовых идентификаторов пользователей (UID) и идентификаторов групп (GID). Иногда их называют идентификаторами процесса. В число этих идентификаторов входят:

• реальный (real) ID пользователя и группы;

• действующий (effective) ID пользователя и группы;

• сохраненный установленный ID пользователя (saved set-user-ID) и сохраненный установленный ID группы (saved set-group-ID);

• характерный для Linux пользовательский и групповой ID файловой системы;

• дополнительные идентификаторы групп.

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


9.1. Реальный идентификатор пользователя и реальный идентификатор группы

Реальные идентификаторы пользователя и группы идентифицируют пользователя и группу, которым принадлежит процесс. При входе в систему оболочка получает свои реальные ID пользователя и группы из третьего и четвертого полей записи в файле /etc/passwd (см. раздел 8.1). При создании нового процесса (например, когда оболочка выполняет программу) он наследует эти идентификаторы у своего родительского процесса.


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

В большинстве реализаций UNIX (Linux, как объясняется в разделе 9.5, в этом плане от них немного отличается) действующие UID и GID в совокупности с дополнительными идентификаторами групп используются для определения полномочий, которыми наделен процесс, при его попытке выполнения различных операций (в частности, системных вызовов). Например, эти идентификаторы определяют полномочия, которыми процесс наделен при доступе к таким ресурсам, как файлы и объекты межпроцессного взаимодействия (IPC) в System V. У таких объектов, в частности, есть собственные связанные с ними пользовательские и групповые идентификаторы, определяющие их принадлежность. В разделе 20.5 будет показано, что действующий UID также проверяется ядром для определения того, может ли один процесс отправить сигнал другому.

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

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

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


9.3. Программы с установленным идентификатором пользователя и установленным идентификатором группы

Программа с установленным идентификатором пользователя позволяет процессу получить полномочия, которые он обычно не получает, путем установки действующего ID пользователя на то же значение, которое имеется у идентификатора пользователя (владельца) исполняемого файла. Программа с установленным ID группы выполняет аналогичную задачу для принадлежащего процессу действующего идентификатора группы. (Выражения «программа с установленным идентификатором пользователя» и «программа с установленным идентификатором группы» иногда сокращают до видов «set-UID-программа» и «set-GID-программа».)

Как и любой другой файл, файл исполняемой программы имеет связанный с ним идентификатор пользователя и идентификатор группы, которые определяют принадлежность файла. Кроме того, у исполняемого файла имеется два специальных бита полномочий: бит установленного идентификатора пользователя (set-user-ID) и бит установленного идентификатора группы (set-group-ID). (В действительности эти два бита полномочий есть у каждого файла, но нас здесь интересует их использование применительно к исполняемым файлам.) Эти биты полномочий устанавливаются командой chmod. Непривилегированный пользователь может устанавливать эти биты для тех файлов, которыми он владеет. Привилегированный пользователь (CAP_FOWNER) может устанавливать эти биты для любого файла. Рассмотрим пример:

$ su

Password:

# ls — l prog

— rwxr-xr-x 1 root root 302585 Jun 26 15:05 prog

# chmod u+s prog Установка бита полномочий set-user-ID

# chmod g+s prog Установка бита полномочий set-group-ID

Как показано в этом примере, у программы могут быть установлены оба этих бита, хотя такое встречается нечасто. Когда для вывода списка полномочий программы, имеющей установленный бит set-user-ID или set-group-ID, используется команда ls — l, в нем буква x, которая обычно применяется для демонстрации установки полномочия на выполнение, заменяется буквой s:

# ls — l prog

— rwsr-sr-x 1 root root 302585 Jun 26 15:05 prog

Когда set-user-ID-программа запускается (то есть загружается в память процесса с помощью команды exec()), ядро устанавливает для действующего пользовательского ID точно такое же значение, что и у пользовательского ID исполняемого файла. Запуск программы с полномочиями setgid имеет такой же эффект относительно действующего группового идентификатора процесса. Изменение действующего пользовательского или группового ID таким способом дает процессу (а иными словами, пользователю, для которого выполняется программа) полномочия, которые он не имел бы при других обстоятельствах. Например, если исполняемый файл принадлежит пользователю по имени root (привилегированному пользователю) и имеет установленный бит set-user-ID, то процесс при запуске программы обретает полномочия суперпользователя.

Программы с полномочиями setuid и setgid могут также использоваться с целью смены действующих идентификаторов процесса на какие-либо другие, отличные от root. Например, чтобы предоставить доступ к защищенному файлу (или к другому системному ресурсу), может быть достаточно создать специально предназначенный для этого ID пользователя (группы) с полномочиями, требуемыми для доступа к файлу, и создать программу с полномочиями setuid (setgid), изменяющую действующий пользовательский (групповой) ID на этот идентификатор. Это даст программе полномочия по доступу к файлу без предоставления ей всех полномочий привилегированного пользователя.

Иногда мы будем использовать выражение set-user-ID-root, чтобы отличать set-user-ID-программу, владельцем которой является root, от программы, которой владеет другой пользователь и которая просто дает процессу полномочия, предоставляемые этому пользователю.

Теперь мы станем употреблять слово «привилегированный» в двух разных смыслах. Первый мы определили ранее: это процесс с действующим идентификатором пользователя со значением 0, у которого имеются все полномочия, присущие пользователю по имени root. Но, когда речь заходит о set-user-ID-программе, владельцем которой является другой, не root-пользователь, то мы называем процесс наделенным полномочиями, соответствующими идентификатору пользователя set-user-ID-программы. Какой именно смысл вкладывается в понятие «привилегированный», в каждом случае будет понятно из контекста.

По причинам, объясняемым в разделе 38.3, биты полномочий set-user-ID и set-group-ID не оказывают никакого влияния на используемые в Linux сценарии оболочки.

В качестве примеров часто используемых в Linux set-user-ID-программ можно привести passwd(1), изменяющую пользовательский пароль, mount(8) и umount(8), которые занимаются монтированием и размонтированием файловых систем, и su(1), которая позволяет пользователю запускать оболочку под различными UID. В качестве примера программы с полномочиями setgid можно привести wall(1), которая записывает сообщение на все терминалы, владельцами которых является группа tty (обычно она является владельцем каждого терминала).

В разделе 8.5 уже отмечалось, что программа из листинга 8.2 должна быть запущена под учетной записью root, чтобы получить доступ к файлу /etc/shadow. Эту программу можно сделать запускаемой любым пользователем, назначив ее set-user-ID-root-программой:

$ su

Password:

# chown root check_password Закрепление владения этой программой за root

# chmod u+s check_password С установленным битом set-user-ID

# ls — l check_password

— rwsr-xr-x 1 root users 18150 Oct 28 10:49 check_password

# exit

$ whoami Это непривилегированный пользователь

mtk

$ ./check_password Но теперь мы можем получить доступ к файлу

Username: avr теневых паролей, используя эту программу

Password:

Successfully authenticated: UID=1001

Технология set-user-ID/set-group-ID является полезным и эффективным средством, но при недостаточно тщательно спроектированных приложениях может создать бреши в системе безопасности. Практические наработки, которых следует придерживаться при написании программ с полномочиями setuid и setgid, перечисляются в главе 38.


9.4. Сохраненный set-user-ID и сохраненный set-group-ID

Сохраненный установленный идентификатор пользователя (set-user-ID) и сохраненный установленный идентификатор группы (set-group-ID) предназначены для применения с программами с полномочиями setuid и setgid. При выполнении программы наряду со многими другими происходят и следующие действия.

1. Если у исполняемого файла установлен бит полномочий set-user-ID (set-group-ID), то действующий пользовательский (групповой) ID процесса становится таким же, что и у владельца исполняемого файла. Если у исполняемого файла не установлен бит полномочий set-user-ID (set-group-ID), то действующий пользовательский (групповой) ID процесса не изменяется.

2. Значения для сохраненного set-user-ID и сохраненного set-group-ID копируются из соответствующих действующих идентификаторов. Это копирование осуществляется независимо от того, был ли у выполняемого на данный момент файла установлен бит set-user-ID или бит set-group-ID.

Рассмотрим пример того, что происходит в ходе вышеизложенных действий. Предположим, что процесс, чьи пользовательские идентификаторы — реальный, действительный и сохраненный set-user-ID — равны 1000, выполняет set-user-ID-программу, владельцем которой является root (UID равен 0). После выполнения пользовательские идентификаторы процесса будут изменены следующим образом:

real=1000 effective=0 saved=0 (реальный=1000 действующий=0 сохраненный=0)

Различные системные вызовы позволяют set-user-ID-программе переключать ее действующий пользовательский идентификатор между значениями реального UID и сохраненного set-user-ID. Аналогичные системные вызовы позволяют программе с полномочиями setgid изменять ее действующий GID. Таким образом, программа может временно сбросить и восстановить любые полномочия, связанные с пользовательским (групповым) идентификатором исполняемого файла. (Иными словами, она может перемещаться между состояниями потенциальной привилегированности и фактической работы с полномочиями.) При более подробном рассмотрении вопроса в разделе 38.2 выяснится, что требования безопасного программирования гласят: программа должна работать под непривилегированным (реальным) ID до тех пор, пока ей на самом деле не понадобятся права привилегированного (то есть сохраненного установленного) ID.

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

Сохраненные установленные идентификаторы являются нововведениями, появившимися в System V и принятыми в POSIX. В выпусках BSD, предшествующих 4.4, они не предоставлялись. В исходном стандарте POSIX.1 поддержка этих идентификаторов была необязательной, но в более поздних стандартах (начиная с FIPS 151-1 в 1988 году) стала обязательной.


9.5. Пользовательские и групповые ID файловой системы

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

Обычно пользовательские и групповые идентификаторы файловой системы имеют те же значения, что и соответствующие действующие идентификаторы (и, таким образом, нередко совпадают с соответствующими реальными идентификаторами). Более того, когда изменяется действующий пользовательский или групповой ID (либо посредством системного вызова, либо из-за выполнения программы с полномочиями setuid или setgid), изменяется, получая такое же значение, и соответствующий идентификатор файловой системы. Поскольку идентификаторы файловой системы следуют таким образом за действующими идентификаторами, это означает, что Linux при проверке привилегий и полномочий фактически ведет себя точно так же, как любая другая реализация UNIX. Лишь когда используются два характерных для Linux системных вызова — setfsuid() и setfsgid(), поведение Linux отличается от поведения других реализаций UNIX, и ID файловой системы отличаются от соответствующих действующих идентификаторов.

Зачем в Linux предоставляются идентификаторы файловой системы и при каких обстоятельствах нам понадобятся разные значения для действующих идентификаторов и идентификаторов файловой системы? Причины главным образом имеют исторические корни. Идентификаторы файловой системы впервые появились в Linux 1.2. В этой версии ядра один процесс мог отправлять сигнал другому, лишь если действующий идентификатор пользователя отправителя совпадал с реальным или действующим идентификатором пользователя целевого процесса. Это повлияло на некоторые программы, например на программу сервера Linux NFS (Network File System — сетевая файловая система), которой нужна была возможность доступа к файлам, как будто у нее есть действующие идентификаторы соответствующих клиентских процессов. Но, если бы NFS-сервер изменял свой действующий идентификатор пользователя, он стал бы уязвим от сигналов непривилегированных пользовательских процессов. Для предотвращения этой возможности были придуманы отдельные пользовательские и групповые ID файловой системы. Оставляя неизмененными свои действующие идентификаторы, но изменяя идентификаторы файловой системы, NFS-сервер может выдавать себя за другого пользователя с целью обращения к файлам без уязвимости от сигналов пользовательских процессов.

Начиная с версии ядра 2.0, в Linux приняты установленные SUSv3 правила относительно разрешений на отправку сигналов. Эти правила не касаются действующего ID пользователя целевого процесса (см. раздел 20.5). Таким образом, наличие идентификатора файловой системы утратило свою актуальность (теперь процесс может удовлетворить возникающие потребности, изменив значение действующего пользовательского ID на ID привилегированного пользователя (и обратно) путем разумного применения системных вызовов, рассматриваемых далее в этой главе). Но эта возможность по-прежнему предусмотрена для сохранения совместимости с существующим программным обеспечением.

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


9.6. Дополнительные групповые идентификаторы

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


9.7. Извлечение и модификация идентификаторов процессов

В Linux для извлечения и изменения различных пользовательских и групповых идентификаторов, рассматриваемых в данной главе, предоставляется ряд системных вызовов и библиотечных функций. В SUSv3 определяется только часть этих API. Из оставшихся некоторые широко доступны в иных реализациях UNIX, а другие характерны только для Linux. По мере рассмотрения каждого интерфейса мы также будем обращать внимание на вопросы портируемости. Ближе к концу главы в табл. 9.1 мы перечислим операции всех интерфейсов, используемых для изменения идентификаторов процессов.

В качестве альтернативы применения системных вызовов, описываемых на следующих страницах, идентификаторы любого процесса могут быть определены путем анализа строк Uid, Gid и Groups, предоставляемых Linux-файлом /proc/PID/status. В строках Uid и Gid перечисляются идентификаторы в следующем порядке: реальный, действующий, сохраненный установленный и идентификатор файловой системы.

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

• CAP_SETUID позволяет процессу произвольно менять свои пользовательские идентификаторы.

• CAP_SETGID позволяет процессу произвольно изменять свои групповые идентификаторы.


9.7.1. Извлечение и изменение реальных, действующих и сохраненных установленных идентификаторов

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


Извлечение реальных и действующих идентификаторов

Системные вызовы getuid() и getgid() возвращают соответственно реальный пользовательский идентификатор и реальный идентификатор группы вызывающего процесса. Системные вызовы geteuid() и getegid() выполняют соответствующие задачи для действующих идентификаторов. Эти системные вызовы всегда завершаются успешно.

#include <unistd.h>


uid_t getuid(void);

Возвращает реальный идентификатор пользователя вызывающего процесса

uid_t geteuid(void);

Возвращает действительный идентификатор пользователя вызывающего процесса

gid_t getgid(void);

Возвращает реальный идентификатор группы вызывающего процесса

gid_t getegid(void);

Возвращает действующий идентификатор группы вызывающего процесса


Изменение действующих идентификаторов

Системный вызов setuid() изменяет действующий идентификатор пользователя, и, возможно, реальный ID пользователя и сохраненный установленный ID пользователя вызывающего процесса, присваивая значение, заданное его аргументом uid. Системный вызов setgid() выполняет аналогичную задачу для соответствующих идентификаторов группы.

#include <unistd.h>


int setuid(uid_t uid);

int setgid(gid_t gid);

Оба возвращают 0 при успешном завершении и –1 — при ошибке

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

1. Когда вызов setuid() осуществляется непривилегированным процессом, изменяется только действующий пользовательский идентификатор процесса. Кроме того, он может быть изменен только на то же самое значение, которое имеется либо у реального идентификатора пользователя, либо у сохраненного установленного идентификатора пользователя. (Попытки нарушить это ограничение приводят к выдаче ошибки EPERM.) Это означает, что для непривилегированных пользователей данный вызов полезен лишь при выполнении set-user-ID-программы, поскольку при выполнении обычной программы у процесса обнаруживаются одинаковые по значению реальный, действующий и сохраненный установленный пользовательские идентификаторы. В некоторых реализациях, уходящих корнями в BSD, вызовы setuid() или setgid() непривилегированным процессом имеют иную семантику, отличающуюся от применяемой другими реализациями UNIX. В BSD вызовы изменяют реальный, действующий и сохраненный установленный идентификаторы на значение текущего реального или действующего идентификатора.

2. Когда привилегированный процесс выполняет setuid() с ненулевым аргументом, все идентификаторы — реальный, действующий и сохраненный установленный пользовательский ID — получают значение, указанное в аргументе uid. Последствия необратимы, поскольку, как только идентификатор у привилегированного процесса таким образом изменится, процесс утратит все полномочия и не сможет впоследствии воспользоваться setuid(), чтобы снова переключить идентификаторы на нуль. Если такой исход нежелателен, то вместо setuid() нужно воспользоваться либо seteuid(), либо setreuid() — системными вызовами, которые вскоре будут рассмотрены.

Правила, регулирующие изменения, которые могут быть внесены с помощью setgid() в идентификаторы группы, аналогичны рассмотренным, но с заменой setuid() на setgid(), а группы на пользователя. С этими изменениями правило 1 применимо без оговорок. В правиле 2, поскольку изменение группового идентификатора не вызывает потери полномочий (которые определяются значением действующего пользовательского идентификатора, UID), привилегированные программы могут задействовать setgid() для свободного изменения групповых идентификаторов на любые желаемые значения.

Следующий вызов является предпочтительным способом для set-user-ID-root-программы, чей действующий UID в этот момент равен 0, безвозвратно сбросить все полномочия (путем установки как действующего, так и сохраненного установленного пользовательского идентификатора на то же значение, которое имеется у реального UID):

if (setuid(getuid()) == -1)

errExit("setuid");

Set-user-ID-программа, принадлежащая пользователю, отличному от root, может применять setuid() для переключения действующего UID между значениями реального UID и сохраненного установленного UID по соображениям безопасности, рассмотренным в разделе 9.4. Но для этой цели предпочтительнее обратиться к системному вызову seteuid(), поскольку он действует точно так же, независимо от того, принадлежит пользователю по имени root set-user-ID-программа или нет.

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

#include <unistd.h>


int seteuid(uid_t euid);

int setegid(gid_t egid);

Оба возвращают при успешном завершении 0, а при ошибке —1

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

1. Непривилегированный процесс может изменять действующий идентификатор, присваивая ему только то значение, которое соответствует реальному или сохраненному установленному идентификатору. (Иными словами, для непривилегированного процесса функции seteuid() и setegid() произведут тот же эффект, что и функции setuid() и setgid() соответственно, за исключением ранее упомянутых вопросов портируемости на BSD-системы.)

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

Использование seteuid() является предпочтительным методом для программ с полномочиями setuid и setgid с целью временного сброса и последующего восстановления полномочий. Рассмотрим пример.

euid = geteuid(); /* Сохранение исходного действующего UID

(совпадает с установленным UID) */

if (seteuid(getuid()) == -1) /* Сброс полномочий */

errExit("seteuid");

if (seteuid(euid) == -1) /* Возвращение полномочий */

errExit("seteuid");

Изначально происходящие из BSD, функции seteuid() и setegid() теперь определены в SUSv3 и встречаются во многих реализациях UNIX.

В старых версиях библиотеки GNU C (glibc 2.0 и более ранние) выражение seteuid(euid) было реализовано в виде setreuid(–1, euid). В современных версиях glibc функция seteuid(euid) реализована в виде setresuid(–1, euid, –1). (Функции setreuid(), setresuid() и их аналоги по работе с групповыми идентификаторами будут вскоре рассмотрены.) Обе реализации позволяют нам указать в качестве euid такое же значение, которое на данный момент имеется у действующего идентификатора пользователя (то есть не требовать изменения). Но в SUSv3 такое поведение для seteuid() не определено, и получить его в некоторых реализациях UNIX невозможно. Как правило, различие в поведении разных реализаций не проявляется, так как в обычных условиях действующий идентификатор пользователя имеет то же самое значение, которое имеется либо у реального идентификатора пользователя, либо у сохраненного установленного идентификатора пользователя. (Единственный способ, позволяющий сделать в Linux действующий ID пользователя отличным как от реального ID пользователя, так и от сохраненного установленного ID пользователя, предусматривает применение нестандартного системного вызова setresuid().)

Во всех версиях glibc (включая современные) setegid(egid) реализуется в виде setregid(–1, egid). Как и в случае использования seteuid(), это означает, что мы можем указать для egid такое же значение, которое на данный момент имеется у действующего идентификатора группы, хотя такое поведение не указано в SUSv3. Это также означает, что setegid() изменяет сохраненный установленный ID группы, если для действующего ID группы установлено значение, отличное от имеющегося на данный момент у реального ID группы. (То же самое можно сказать и о более старых реализациях seteuid(), использующих setreuid().) Это поведение также не указано в SUSv3.


Изменение реальных и действующих идентификаторов

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

#include <unistd.h>


int setreuid(uid_t ruid, uid_t euid);

int setregid(gid_t rgid, gid_t egid);

Оба при успешном завершении возвращают 0 или –1 при ошибке

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

Первоначально появившись в BSD, теперь setreuid() и setregid() указаны в SUSv3 и доступны в большинстве реализаций UNIX.

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

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

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

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

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

1) значение ruid не равно –1 (то есть для реального идентификатора пользователя устанавливается в точности то же значение, которое у него уже имелось);

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

С другой стороны, если процесс использует setreuid() только для изменения действующего идентификатора пользователя на то же значение, которое имеется на данный момент у реального ID пользователя, то сохраненный установленный ID пользователя остается неизмененным, и последующий вызов setreuid() (или seteuid()) может восстановить действующий ID пользователя, присвоив ему значение сохраненного установленного ID пользователя. (В SUSv3 не определяется влияние от применения setreuid() и setregid() на сохраненные установленные идентификаторы пользователя, но в SUSv4 указывается только что рассмотренное поведение.)

Третье правило предоставляет способ, позволяющий set-user-ID-программам лишаться своего привилегированного состояния безвозвратно, с помощью следующего вызова:

setreuid(getuid(), getuid());

Процесс с установленным идентификатором привилегированного пользователя (set-user-ID-root), которому нужно изменить как свои пользовательские, так и групповые полномочия на произвольные значения, должен вызвать сначала setregid(), а затем setreuid(). Если вызов делается в обратном порядке, вызов setregid() даст сбой, потому что после вызова setregid() программа уже не будет привилегированной. Те же замечания применимы к системным вызовам setresuid() и setresgid() (рассматриваемым ниже), если они используются для достижения аналогичной цели.

Выпуски BSD до 4.3BSD включительно не имели сохраненного установленного идентификатора пользователя и сохраненного установленного идентификатора группы (наличие которых теперь предписывается в SUSv3). Вместо этого в BSD системные вызовы setreuid() и setregid() позволяли процессу сбрасывать и восстанавливать полномочия, меняя местами значения реального и действующего идентификаторов в обе стороны. В результате возникал нежелательный побочный эффект изменения реального идентификатора пользователя с целью изменения действительного идентификатора пользователя.


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

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

#define _GNU_SOURCE

#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);

Оба при успешном завершении возвращают 0 или –1 при ошибке

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


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

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

#define _GNU_SOURCE

#include <unistd.h>


int setresuid(uid_t ruid, uid_t euid, uid_t suid);

int setresgid(gid_t rgid, gid_t egid, gid_t sgid);

Оба при успешном завершении возвращают 0 или –1 при ошибке

Если не нужно изменять все идентификаторы, для того из них, который не требует изменений, указывается значение –1 аргумента. Например, следующий вызов эквивалентен seteuid(x):

setresuid(-1, x, — 1);

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

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

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

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

Вызовы setresuid() и setresgid() делают «все или ничего» Либо успешно изменяются все запрошенные идентификаторы, либо не изменяется ни один из них. (То же самое можно сказать и о других системных вызовах, рассмотренных в этой главе и изменяющих сразу несколько идентификаторов.)

Хотя setresuid() и setresgid() предоставляют самый очевидный API для изменения идентификаторов процесса, невозможно применять их портируемым образом в приложениях — они не определены в SUSv3 и доступны только в немногих других реализациях UNIX.


9.7.2. Извлечение и изменение идентификаторов файловой системы

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

#include <sys/fsuid.h>


int setfsuid(uid_t fsuid);

Всегда возвращает предыдущий пользовательский идентификатор файловой системы

int setfsgid(gid_t fsgid);

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

Системный вызов setfsuid() изменяет пользовательский идентификатор файловой системы процесса на значение, указанное в аргументе fsuid. Системный вызов setfsgid() изменяет групповой идентификатор файловой системы на значение, указанное в аргументе fsgid.

Здесь также применяются некоторые правила. Правила для setfsgid() аналогичны правилам для setfsuid() и звучат таким образом.

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

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

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

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


9.7.3. Извлечение и изменение дополнительных групповых идентификаторов

Системный вызов getgroups() записывает в массив, указанный в аргументе grouplist, набор групп, в которые на данный момент входит вызывающий процесс.

#include <unistd.h>


int getgroups(int gidsetsize, gid_t grouplist[]);

Возвращает при успешном завершении количество групповых идентификаторов, помещенное в grouplist, а при ошибке —1

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

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

Если количество групп, в который входит процесс, превышает значение, указанное в gidsetsize, системный вызов getgroups() возвращает ошибку (EINVAL). Во избежание этого можно задать для массива grouplist значение, большее на единицу (для разрешения портируемости при возможном включении действующего группового идентификатора), чем значение константы NGROUPS_MAX (определенной в заголовочном файле <limits.h>). Эта константа определяет максимальное количество дополнительных групп, в которые может входить процесс. Таким образом, grouplist можно объявить с помощью следующего выражения:

gid_t grouplist[NGROUPS_MAX + 1];

В ядрах Linux, предшествующих версии 2.6.4, у NGROUPS_MAX было значение 32. Начиная с версии 2.6.4, значение у NGROUPS_MAX стало равно 65536.

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

• вызвать sysconf(_SC_NGROUPS_MAX) (использование sysconf() рассматривается в разделе 11.2);

• считать ограничение из предназначенного только для чтения и характерного только для Linux файла /proc/sys/kernel/ngroups_max. Этот файл предоставляется ядрами, начиная с версии 2.6.4.

Кроме этого, приложение может выполнить вызов getgroups(), указав в качестве аргумента gidsetsize значение 0. В этом случае grouplist не изменяется, но возвращаемое вызовом значение содержит количество групп, в которые входит процесс.

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

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

#define _BSD_SOURCE

#include <grp.h>


int setgroups(size_t gidsetsize, const gid_t *grouplist);

int initgroups(const char *user, gid_t group);

Оба возвращают при успешном завершении 0, а при ошибке —1

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

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

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

Хотя в SUSv3 системные вызовы setgroups() и initgroups() не фигурируют, они доступны во всех реализациях UNIX.


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

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


Таблица 9.1. Сводные данные по интерфейсам, используемым для изменения идентификаторов процесса

Интерфейс

Назначение и действие в:

Портируемость

Непривилегированном процессе

Привилегированном процессе

setuid(u)

setgid(g)

Изменение действующего ID на такое же значение, что и у текущего реального или сохраненного установленного ID

Изменение реального, действующего и сохраненного установленного ID на любое (единое) значение

Вызовы указываются в SUSv3; у вызовов, берущих происхождение от BSD, другая семантика

seteuid(e)

setegid(e)

Изменение действующего ID на такое же значение, что и у текущего реального или сохраненного установленного ID

Изменение действующего ID на любое значение

Вызовы указываются в SUSv3

setreuid(r, e)

setregid(r, e)

(Независимое) изменение реального ID на такое же значение, что и у текущего реального или действующего ID, и действующего ID на такое же значение, что у текущего реального, действующего или сохраненного установленного ID

(Независимое) изменение реального и действующего ID на любое значение

Вызовы указываются в SUSv3, но в различных реализациях работают по-разному

setresuid(r, e, s)

setresgid(r, e, s)

Изменение ID файловой системы на то же значение, что и у текущего реального, действительного, сохраненного установленного ID или ID файловой системы

Изменение ID файловой системы на любое значение

Вызовы характерны только для Linux

setgroups(n, l)

Этот системный вызов не может быть сделан из непривилегированных процессов

Установка для дополнительных групповых ID любых значений

Этот системный вызов в SUSv3 не фигурирует, но доступен во всех реализациях UNIX

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

• Имеющиеся в glibc реализации seteuid() и setegid() также позволяют устанавливать для действующего идентификатора такое же значение, какое у него и было, но эта особенность в SUSv3 не упоминается.

• Если при вызовах setreuid() и setregid() как привилегированными, так и непривилегированными процессами, до осуществления вызовов значение r (реального идентификатора) не равно –1 или для e (действующего идентификатора) указано значение, отличное от значения реального идентификатора, то сохраненный установленный пользовательский или сохраненный установленный групповой ID также устанавливаются на то же значение, что и у нового действующего идентификатора. (В SUSv3 не указано, что setreuid() и setregid() вносят изменения в сохраненные установленные ID.)

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

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



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


9.7.5. Пример: вывод на экран идентификаторов процесса

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


Листинг 9.1. Отображение на экране всех пользовательских и групповых идентификаторов процесса

proccred/idshow.c

#define _GNU_SOURCE

#include <unistd.h>

#include <sys/fsuid.h>

#include <limits.h>

#include "ugid_functions.h" /* userNameFromId() и groupNameFromId() */

#include "tlpi_hdr.h"


#define SG_SIZE (NGROUPS_MAX + 1)

int

main(int argc, char *argv[])

{

uid_t ruid, euid, suid, fsuid;

gid_t rgid, egid, sgid, fsgid;

gid_t suppGroups[SG_SIZE];

int numGroups, j;

char *p;


if (getresuid(&ruid, &euid, &suid) == -1)

errExit("getresuid");

if (getresgid(&rgid, &egid, &sgid) == -1)

errExit("getresgid");


/* Попытки изменения идентификаторов файловой системы для непривилегированных

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

возвращают текущие идентификаторы файловой системы */


fsuid = setfsuid(0);

fsgid = setfsgid(0);


printf("UID: ");

p = userNameFromId(ruid);

printf("real=%s (%ld); ", (p == NULL)?"???": p, (long) ruid);

p = userNameFromId(euid);

printf("eff=%s (%ld); ", (p == NULL)?"???": p, (long) euid);

p = userNameFromId(suid);

printf("saved=%s (%ld); ", (p == NULL)?"???": p, (long) suid);

p = userNameFromId(fsuid);

printf("fs=%s (%ld); ", (p == NULL)?"???": p, (long) fsuid);

printf("\n");


printf("GID: ");

p = groupNameFromId(rgid);

printf("real=%s (%ld); ", (p == NULL)?"???": p, (long) rgid);

p = groupNameFromId(egid);

printf("eff=%s (%ld); ", (p == NULL)?"???": p, (long) egid);

p = groupNameFromId(sgid);

printf("saved=%s (%ld); ", (p == NULL)?"???": p, (long) sgid);

p = groupNameFromId(fsgid);

printf("fs=%s (%ld); ", (p == NULL)?"???": p, (long) fsgid);

printf("\n");


numGroups = getgroups(SG_SIZE, suppGroups);

if (numGroups == -1)

errExit("getgroups");


printf("Supplementary groups (%d): ", numGroups);

for (j = 0; j < numGroups; j++) {

p = groupNameFromId(suppGroups[j]);

printf("%s (%ld) ", (p == NULL)?"???": p, (long) suppGroups[j]);

}

printf("\n");


exit(EXIT_SUCCESS);

}

proccred/idshow.c


9.8. Резюме

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

Когда запускается set-user-ID-программа, действующий пользовательский идентификатор процесса устанавливается на то значение, которое имеется у владельца файла. Этот механизм позволяет пользователю присвоить идентификатор, а следовательно, и полномочия другого пользователя при запуске конкретной программы. Аналогично, программы с полномочиями setgid изменяют действующий групповой ID процесса, в котором выполняется программа. Сохраненный установленный идентификатор пользователя (saved set-user-ID) и сохраненный установленный идентификатор группы (saved set-group-ID) позволяют программам с полномочиями setuid и setgid временно сбрасывать, а затем позже восстанавливать полномочия.

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


9.9. Упражнения

9.1. Предположим, что в каждом из следующих случаев исходный набор пользовательских идентификаторов процесса такой: реальный = 1000, действующий = 0, сохраненный = 0, файловой системы = 0. Какими станут пользовательские идентификаторы после следующих вызовов:

1) setuid(2000);

2) setreuid(–1, 2000);

3) seteuid(2000);

4) setfsuid(2000);

5) setresuid(–1, 2000, 3000)?

9.2. Является ли привилегированным процесс со следующими идентификаторами пользователя? Обоснуйте ответ.

real=0 effective=1000 saved=1000 file-system=1000

9.3. Реализуйте функцию initgroups(), используя setgroups() и библиотечные функции, для извлечения информации из файлов паролей и групп (см. раздел 8.4). Не забудьте, что для возможности вызова setgroups() процесс должен быть привилегированным.

9.4. Если процесс, чьи пользовательские идентификаторы имеют одинаковое значение X, выполняет set-user-ID-программу, пользовательский идентификатор которой равен Y и имеет ненулевое значение, то полномочия процесса устанавливаются следующим образом:

real=X effective=Y saved=Y

(Мы игнорируем пользовательский идентификатор файловой системы, поскольку его значение следует за действующим идентификатором пользователя.) Запишите соответственно вызовы setuid(), seteuid(), setreuid() и setresuid(), которые будут применяться для выполнения таких операций, как:

1) приостановление и возобновление set-user-ID-идентичности (то есть переключение действующего идентификатора пользователя на значение реального пользовательского идентификатора, а затем возвращение к сохраненному установленному идентификатору пользователя);

2) безвозвратный сброс set-user-ID-идентичности (то есть гарантия того, что для действующего пользовательского идентификатора и сохраненного установленного идентификатора пользователя устанавливается значение реального идентификатора пользователя).

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

9.5. Повторите предыдущее упражнение для процесса выполнения set-user-ID-root-программы, у которой следующий исходный набор идентификаторов процесса:

real=X effective=0 saved=0

10. Время

При выполнении программы нас могут интересовать два вида времени.

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

• Время процесса. Это продолжительность использования процессом центрального процессора. Замеры времени процесса нужны для проверки или оптимизации производительности программы либо алгоритма.

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


10.1. Календарное время

В зависимости от географического местоположения, внутри систем UNIX время представляется отмеренным в секундах от начала его отсчета (Epoch): от полуночи 1 января 1970 года, по всемирному координированному времени — Universal Coordinated Time (UTC, ранее называвшемуся средним временем по Гринвичу — Greenwich Mean Time, или GMT). Примерно в это время начали свое существование системы UNIX. Календарное время сохраняется в переменных типа time_t, который относится к целочисленным типам, указанным в SUSv3.

В 32-разрядных системах Linux тип time_t, относящийся к целочисленным типам со знаком, позволяет представлять даты в диапазоне от 13 декабря 1901 года, 20:45:52, до 19 января 2038 года, 03:14:07. (В SUSv3 нет определения отрицательного значения типа time_t.) Таким образом, многие имеющиеся на сегодня 32-разрядные системы UNIX сталкиваются с теоретически возможной проблемой 2038 года, которую им предстоит решить до его наступления, если они в будущем будут выполнять вычисления, связанные с датами. Эту проблему существенно смягчает уверенность в том, что к 2038 году все системы UNIX станут, скорее всего, 64-разрядными или даже более высокой разрядности. Но встроенные 32-разрядные системы, век которых продлится, видимо, намного дольше, чем представлялось поначалу, все же могут столкнуться с этой проблемой. Кроме того, она останется неразрешенной для любых устаревших данных и приложений, работающих со временем в 32-разрядном формате time_t.

Системный вызов gettimeofday() возвращает календарное время в буфер, на который указывает значение аргумента tv.

#include <sys/time.h>


int gettimeofday(struct timeval *tv, struct timezone *tz);

Возвращает 0 при успешном завершении или –1 при ошибке

Аргумент tv является указателем на структуру следующего вида:

struct timeval {

time_t tv_sec; /* Количество секунд с 00:00:00, 1 янв 1970 UTC */

suseconds_t tv_usec; /* Дополнительные микросекунды (long int) */

};

Хотя для поля tv_usec предусмотрена микросекундная точность, конкретная точность возвращаемого в нем значения определяется реализацией, зависящей от архитектуры системы. (Буква «u» в tv_usec произошла от сходства с греческой буквой μ («мю»), используемой в метрической системе для обозначения одной миллионной доли.) В современных системах x86-32 (то есть в системах типа Pentium с регистром счетчика меток реального времени — Timestamp Counter, значение которого увеличивается на единицу с каждым тактовым циклом центрального процессора), вызов gettimeofday() предоставляет микросекундную точность.

Аргумент tz в вызове gettimeofday() является историческим артефактом. В более старых реализациях UNIX он использовался в целях извлечения для системы информации о часовом поясе (timezone). Сейчас этот аргумент уже вышел из употребления и в качестве его значения нужно всегда указывать NULL.

При предоставлении аргумента tz возвращается структура timezone, в чьих полях содержатся значения, указанные в устаревшем аргументе tz предшествующего вызова settimeofday(). Структура включает два поля: tz_minuteswest и tz_dsttime. Поле tz_minuteswest показывает количество минут, которое нужно добавить в этом часовом поясе (zone) для соответствия UTC; отрицательное значение показывает коррекцию в минутах по отношению к востоку от UTC (например, для ценральноевропейского времени это на один час больше, чем UTC, и поле будет содержать значение –60). Поле tz_dsttime содержит константу, придуманную для представления режима летнего времени — day-light saving time (DST), вводимого в этом часовом поясе. Дело в том, что режим летнего времени в устаревшем аргументе tz не может быть представлен с помощью простого алгоритма. (Это поле в Linux никогда не поддерживалось.) Подробности можно найти на странице руководства gettimeofday(2).

Системный вызов time() возвращает количество секунд, прошедших с начала отсчета времени (то есть точно такое же значение, которое возвращает gettimeofday() в поле tv_sec своего аргумента tv).

#include <time.h>


time_t time(time_t *timep);

Возвращает при успешном завершении количество секунд, прошедших с начала отсчета времени, или (time_t) –1 при ошибке

Если значение аргумента timep не равно NULL, количество секунд, прошедшее с начала отсчета времени, также помещается по адресу, который указывает timep.

Поскольку time() возвращает одно и то же значение двумя способами, и единственной возможной ошибкой, которая может произойти при использовании time(), является предоставление неверного адреса в аргументе timep (EFAULT), зачастую применяется такой вызов (без проверки на ошибку):

t = time(NULL);

Причина существования двух системных вызовов (time() и gettimeofday()) с практически одинаковым предназначением имеет исторические корни. В ранних реализациях UNIX предоставлялся системный вызов time(). В 4.2BSD добавился более точный системный вызов gettimeofday(). Существование time() в качестве системного вызова теперь считается избыточным; он может быть реализован в виде библиотечной функции, вызывающей gettimeofday().


10.2. Функции преобразования представлений времени

На рис. 10.1 показаны функции, используемые для преобразования между значениями типа time_t и другими форматами времени, включая его представления для устройств вывода информации. Эти функции ограждают нас от сложностей, привносимых в такие преобразования часовыми поясами, режимами летнего времени и тонкостями локализации. (Часовые пояса будут рассмотрены в разделе 10.3, а вопросы локали — в разделе 10.4.)



Рис. 10.1. Функции для извлечения календарного времени и работы с ним


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

Функция ctime() предоставляет простой метод преобразования значения типа time_t к виду, подходящему для устройств вывода информации.

#include <time.h>


char *ctime(const time_t *timep);

Возвращает при успешном завершении указатель на статически размещенную строку, которая оканчивается символом новой строки и \0, или NULL при ошибке

При предоставлении в timep указателя в виде значения типа time_t функция ctime() возвращает 26-байтовую строку, содержащую, как показано в следующем примере, дату и время в стандартной форме:

Wed Jun 8 14:22:34 2011

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

В SUSv3 утверждается, что вызовы любой из функций — ctime(), gmtime(), localtime() или asctime() — могут перезаписать статически размещенную структуру значениями, возвращенными другими функциями. Иными словами, эти функции могут совместно использовать копии возвращенных массивов из символов и структуру tm, что и делается в некоторых версиях glibc. Если нужно работать с возвращенной информацией в ходе нескольких вызовов этих функций, следует сохранять локальные копии.

Реентерабельная версия ctime() предоставляется в виде ctime_r(). (Реентерабельность рассматривается в подразделе 21.1.2.) Эта функция позволяет вызывающему коду задать дополнительный аргумент — указатель на предоставляемый этим кодом буфер для возвращения строки с данными времени. Другие реентерабельные версии функций, упоминаемые в данной главе, ведут себя точно так же.


10.2.2. Преобразования между time_t и разделенным календарным временем

Функции gmtime() и localtime() преобразуют значение типа time_t в так называемое broken-down time, разделенное календарное время (или время, разбитое на компоненты). Это время помещается в статически размещаемую структуру, чей адрес возвращается в качестве результата выполнения функции.

#include <time.h>


struct tm *gmtime(const time_t *timep);

struct tm *localtime(const time_t *timep);

Обе функции при успешном завершении возвращают указатель на статически размещаемую структуру разделенного календарного времени, а при ошибке — NULL

Функция gmtime() выполняет преобразование календарного времени в разделенное время, соответствующее UTC. (Буквы gm происходят от понятия Greenwich Mean Time.) Напротив, функция localtime() учитывает настройки часового пояса и режима летнего времени, чтобы возвратить разбитое на компоненты время, соответствующее местному системному времени.

Реентерабельные версии этих функций предоставляются в виде gmtime_r() и localtime_r().

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

struct tm {

int tm_sec; /* Секунды (0–60) */

int tm_min; /* Минуты (0–59) */

int tm_hour; /* Часы (0–23) */

int tm_mday; /* День месяца (1–31) */

int tm_mon; /* Месяц (0–11) */

int tm_year; /* Год с 1900 года */

int tm_wday; /* День недели (воскресенье = 0)*/

int tm_yday; /* День в году (0–365; 1 января = 0)*/

int tm_isdst; /* Флаг летнего времени

> 0: летнее время действует;

= 0: летнее время не действует;

< 0: информация о летнем времени недоступна */

};

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

Если определен макрос проверки возможностей _BSD_SOURCE, определяемая библиотекой glibc структура tm также включает два дополнительных поля с более подробной информацией о представленном времени. Первое из них, long int tm_gmtoff, содержит количество секунд, на которое представленное время отстоит на восток от UTC. Второе поле, const char *tm_zone, является сокращенным названием часового пояса (например, CEST для центральноевропейского летнего времени). Ни одно из этих полей в SUSv3 не упоминается, и они появляются лишь в нескольких других реализациях UNIX (в основном происходящих от BSD).

Функция mktime() преобразует местное время, разбитое на компоненты, в значение типа time_t, которое возвращается в качестве результата ее работы. Вызывающий код предоставляет разбитое на компоненты время в структуре tm, на которую указывает значение аргумента timeptr. В ходе этого преобразования поля tm_wday и tm_yday вводимой tm-структуры игнорируются.

#include <time.h>


time_t mktime(struct tm *timeptr);

Возвращает при успешном завершении количество секунд, прошедшее с начала отсчета времени и соответствующее содержимому, на которое указывает timeptr, или значение (time_t) –1 при ошибке

Функция mktime() может изменить структуру, на которую указывает аргумент timeptr. Как минимум, она гарантирует, что для полей tm_wday и tm_yday будут установлены значения, соответствующие значениям других вводимых полей.

Кроме того, mktime() не требует, чтобы другие поля структуры tm ограничивались рассмотренными ранее диапазонами. Для каждого поля, чье значение выходит за границы диапазона, функция mktime() скорректирует это значение таким образом, чтобы оно попало в диапазон, и сделает соответствующие корректировки других полей. Все эти настройки выполняются до того, как mktime() обновляет значения полей tm_wday и tm_yday и вычисляет возвращаемое значение времени с типом time_t.

Например, если вводимое поле tm_sec хранило значение 123, тогда по возвращении из функции значением поля станет 3, а к предыдущему значению поля tm_min будет добавлено 2. (И если это добавление приведет к переполнению tm_min, значение tm_min будет скорректировано, увеличится значение поля tm_hour и т. д.) Эти корректировки применяются даже к полям с отрицательными значениями. Например, указание –1 для tm_sec означает 59-ю секунду предыдущей минуты. Данное свойство позволяет выполнять арифметические действия в отношении даты и времени, выраженных в виде отдельных компонентов.

При выполнении преобразования функцией mktime() используется настройка часового пояса. Кроме того, в зависимости от значения вводимого поля tm_isdst, функцией могут учитываться, а могут и не учитываться настройки летнего времени.

• Если поле tm_isdst имеет значение 0, это время рассматривается как стандартное (то есть настройки летнего времени игнорируются, даже если они должны применяться к данному времени года).

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

• Если поле tm_isdst имеет значение меньше нуля, предпринимается попытка определить, должен ли режим летнего времени применяться в это время года. Обычно именно такая установка нам и требуется.

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


10.2.3. Преобразования между разделенным календарным временем и временем в печатном виде

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


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

Функция asctime(), которой в аргументе timeptr передается указатель на структуру, содержащую разделенное время, возвращает указатель на статически размещенную строку, хранящую время в той же форме, в которой оно возвращается функцией ctime().

#include <time.h>


char *asctime(const struct tm *timeptr);

Возвращает при успешном завершении указатель на статически размещенную строку, оканчивающуюся символом новой строки и \0, или NULL при ошибке

В отличие от функции ctime(), установки часового пояса не влияют на работу функции asctime(), поскольку она выполняет преобразование разделенного времени, которое является либо уже локализованным благодаря использованию функции localtime(), либо временем UTC, возвращенным функцией gmtime().

Как и в случае применения функции ctime(), у нас нет средств для управления форматом строки, создаваемой функцией asctime().

Реентерабельная версия функции asctime() предоставляется в виде asctime_r().

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

$ date

Tue Dec 28 16:01:51 CET 2010

$ ./calendar_time

Seconds since the Epoch (1 Jan 1970): 1293548517 (about 40.991 years)

gettimeofday() returned 1293548517 secs, 715616 microsecs

Broken down by gmtime():

year=110 mon=11 mday=28 hour=15 min=1 sec=57 wday=2 yday=361 isdst=0

Broken down by localtime():

year=110 mon=11 mday=28 hour=16 min=1 sec=57 wday=2 yday=361 isdst=0


asctime() formats the gmtime() value as: Tue Dec 28 15:01:57 2010

ctime() formats the time() value as: Tue Dec 28 16:01:57 2010

mktime() of gmtime() value: 1293544917 secs

mktime() of localtime() value: 1293548517 secs На 3600 секунд больше UTC


Листинг 10.1. Извлечение и преобразование значений календарного времени

time/calendar_time.c

#include <locale.h>

#include <time.h>

#include <sys/time.h>

#include "tlpi_hdr.h"


#define SECONDS_IN_TROPICAL_YEAR (365.24219 * 24 * 60 * 60)


int

main(int argc, char *argv[])

{

time_t t;

struct tm *gmp, *locp;

struct tm gm, loc;

struct timeval tv;

t = time(NULL);


printf("Seconds since the Epoch (1 Jan 1970): %ld", (long) t);

printf(" (about %6.3f years)\n", t / SECONDS_IN_TROPICAL_YEAR);

if (gettimeofday(&tv, NULL) == -1)

errExit("gettimeofday");

printf(" gettimeofday() returned %ld secs, %ld microsecs\n",

(long) tv.tv_sec, (long) tv.tv_usec);

gmp = gmtime(&t);

if (gmp == NULL)

errExit("gmtime");

gm = *gmp; /* Сохранение локальной копии, так как содержимое, на которое указывает

*gmp, может быть изменено вызовом asctime() или gmtime() */


printf("Broken down by gmtime():\n");

printf(" year=%d mon=%d mday=%d hour=%d min=%d sec=%d", gm.tm_year,

gm.tm_mon, gm.tm_mday, gm.tm_hour, gm.tm_min, gm.tm_sec);

printf("wday=%d yday=%d isdst=%d\n", gm.tm_wday, gm.tm_yday, gm.tm_isdst);

gm.tm_isdst);

locp = localtime(&t);

if (locp == NULL)

errExit("localtime");

loc = *locp; /* Сохранение локальной копии */


printf("Broken down by localtime():\n");

printf(" year=%d mon=%d mday=%d hour=%d min=%d sec=%d",

loc.tm_year, loc.tm_mon, loc.tm_mday,

loc.tm_hour, loc.tm_min, loc.tm_sec);

printf("wday=%d yday=%d isdst=%d\n\n", loc.tm_wday, loc.tm_yday, loc.tm_isdst);


printf("asctime() formats the gmtime() value as: %s", asctime(&gm));

printf("ctime() formats the time() value as: %s", ctime(&t));

printf("mktime() of gmtime() value: %ld secs\n", (long) mktime(&gm));

printf("mktime() of localtime() value: %ld secs\n", (long) mktime(&loc));

exit(EXIT_SUCCESS);

}

time/calendar_time.c

Функция strftime() предоставляет нам более тонкую настройку управления при преобразовании разделенного календарного времени в печатный вид.

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

#include <time.h>


size_t strftime(char *outstr, size_t maxsize, const char *format,

const struct tm *timeptr);

Возвращает при успешном завершении количество байтов, помещенных в строку, на которую указывает outstr (исключая завершающий нулевой байт), или 0 при ошибке

Строка, возвращенная в буфер, на который указывает outstr, отформатирована в соответствии со спецификаторами, заданными аргументом format. Аргумент maxsize указывает максимальное пространство, доступное в буфере, заданном аргументом outstr. В отличие от ctime() и asctime() функция strftime() не включает в окончание строки символ новой строки (кроме того, что включается в спецификацию формата, указываемую аргументом format).

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

Аргумент format, используемый при вызове strftime(), представляет собой строку по типу той, что задается в функции printf(). Последовательности, начинающиеся с символа процента (%), являются спецификаторами преобразования, которые заменяются различными компонентами даты и времени в соответствии с символом, следующим за символом процента. Предусмотрен довольно обширный выбор спецификаторов преобразования, часть компонентов которого перечислена в табл. 10.1. (Полный перечень можно найти на странице руководства strftime(3).) За исключением особо оговариваемых, все эти спецификаторы преобразования стандартизированы в SUSv3.

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

Зачастую в книге нам придется выводить текущее время в различных демонстрационных программах. Для этого мы предоставляем функцию currTime(), которая возвращает строку с текущим временем, отформатированным функцией strftime() при заданном аргументе format.

#include "curr_time.h"


char *currTime(const char *format);

Возвращает при успешном завершении указатель на статически размещенную строку или NULL при ошибке

Реализация функции currTime() показана в листинге 10.2.


Таблица 10.1. Отдельные спецификаторы преобразования для strftime()

Спецификатор — Описание — Пример

%% — Символ % — %

%a — Сокращенное название дня недели — Tue

%A — Полное название дня недели — Tuesday

%b, %h — Сокращенное название месяца — Feb

%B — Полное название месяца — February

%c — Дата и время — Tue Feb 1 21:39:46 2011

%d — День месяца (две цифры, от 01 до 31) — 01

%D — Дата в американском формате (то же самое, что и %m/%d/%y) — 02/01/11

%e — День месяца (два символа) — _1

%F — Дата в формате ISO (то же самое, что и %Y-%m-%d) — 2011-02-01

%H — Час (24-часовой формат, две цифры) — 21

%I — Час (12-часовой формат, две цифры) — 09

%j — День года (три цифры, от 001 до 366) — 032

%m — Месяц в виде десятичного числа (две цифры, от 01 до 12) — 02

%M — Минута (две цифры) — 39

%p — AM/PM (до полудня/после полудня) — PM

%P — am/pm (GNU-расширение) — pm

%R — Время в 24-часовом формате (то же самое, что и %H:%M) — 21:39

%S — Секунда (от 00 до 60) — 46

%T — Время (то же самое, что и %H:%M:%S) — 21:39:46

%u — Номер дня недели (от 1 до 7, Понедельник = 1) — 2

%U — Номер недели, начинающейся с воскресенья (от 00 до 53) — 05

%w — Номер дня недели (от 0 до 6, воскресенье = 0) — 2

%W — Номер недели, начинающейся с понедельника (от 00 до 53) — 05

%x — Дата (локализированная версия) — 02/01/11

%X — Время (локализированная версия) — 21:39:46

%y — Последние две цифры года — 11

%Y — Год в формате четырех цифр — 2011

%Z — Название часового пояса — CET


Листинг 10.2. Функция, возвращающая строку с текущим временем

time/curr_time.c

#include <time.h>

#include "curr_time.h" /* Объявление определяемых здесь функций */


#define BUF_SIZE 1000

/* Возвращает строку, содержащую текущее время, отформатированное в сооответствии

со спецификацией в 'format' (спецификаторы на странице руководства strftime(3)).

Если 'format' имеет значение NULL, в качестве спецификатора мы используем "%c"

(что дает дату и время, как для ctime(3), но без завершающего символа новой строки).

При ошибке возвращается NULL. */

char *

currTime(const char *format)

{

static char buf[BUF_SIZE]; /* Нереентерабельная */

time_t t;

size_t s;

struct tm *tm;


t = time(NULL);

tm = localtime(&t);


if (tm == NULL)

return NULL;

s = strftime(buf, BUF_SIZE, (format!= NULL)? format: "%c", tm);

return (s == 0)? NULL: buf;

}

time/curr_time.c


Преобразование из печатного вида в разделенное календарное время

Функция strptime() выполняет преобразование, обратное тому, которое делает функция strftime(). Она преобразует строку в виде даты и времени в разделенное календарное время (время, разбитое на компоненты).

#define _XOPEN_SOURCE

#include <time.h>


char *strptime(const char *str, const char *format, struct tm *timeptr);

Возвращает при успешном завершении указатель на следующий необработанный символ в str или NULL при ошибке

Функция strptime() использует спецификацию, заданную в аргументе format, для разбора строки в формате «дата плюс время», указанной в аргументе str. Затем она помещает результат преобразования в разделенное календарное время в структуру, на которую указывает аргумент timeptr.

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

Спецификация формата, заданная функцией strptime(), похожа на ту, что задается scanf(3). В ней содержатся следующие типы символов:

• спецификации преобразования, начинающиеся с символа процента (%);

• пробельные символы, соответствующие нулю или большему количеству пробелов во введенной строке;

• непробельные символы (отличающиеся от %), которые должны соответствовать точно таким же символам во введенной строке.

Спецификации преобразования похожи на те, которые задаются в функции strftime() (см. табл. 10.1). Основное отличие заключается в их более общем характере. Например, спецификаторы %a и %A могут принять название дня недели как в полной, так и в сокращенной форме, а %d или %e могут использоваться для чтения дня месяца, если он может быть выражен одной цифрой с ведущим нулем или без него. Кроме того, регистр символов игнорируется. Например, для названия месяца одинаково подходят May и MAY. Строка %% применяется для соответствия символу процента во вводимой строке. Дополнительные сведения можно найти на странице руководства strptime(3).

Реализация strptime(), имеющаяся в библиотеке glibc, не вносит изменений в те поля структуры tm, которые не инициализированы спецификаторами из аргумента format. Это означает, что для создания одной структуры tm на основе информации из нескольких строк, из строки даты и строки времени, мы можем воспользоваться серией вызовов strptime(). Хотя в SUSv3 такое поведение допускается, оно не является обязательным, и поэтому полагаться на них в других реализациях UNIX не стоит. В портируемом приложении, прежде чем вызвать strptime(), нужно обеспечить наличие в аргументах str и format входящей информации, которая установит все поля получаемой в итоге структуры tm, или же предоставить подходящую инициализацию структуры tm. В большинстве случаев достаточно задать всей структуре нулевые значения, используя функцию memset(). Но нужно иметь в виду, что значение 0 в поле tm_mday соответствует в glibc-версии и во многих других реализациях функции преобразования времени последнему дню предыдущего месяца. И наконец, следует учесть, что strptime() никогда не устанавливает значение имеющегося в структуре tm для аргумента tm_isdst.

GNU-библиотека C также предоставляет две другие функции, которые служат той же цели, что и strptime(): это getdate() (широкодоступная и указанная в SUSv3) и ее реентерабельный аналог getdate_r() (не указанный в SUSv3 и доступный только в некоторых других реализациях UNIX). Здесь эти функции не рассматриваются, потому что они для указания формата, применяемого при сканировании даты, используют внешний файл (указываемый с помощью переменной среды DATEMSK), что затрудняет их применение, а также создает бреши безопасности в set-user-ID-программах.

Использование функций strptime() и strftime() показано в программе, код которой приводится в листинге 10.3. Эта программа получает аргумент командной строки с датой и временем, преобразует их в календарное время, разбитое на компоненты, с помощью функции strptime(), а затем выводит результат обратного преобразования, выполненного функцией strftime(). Программа получает три аргумента, два из которых обязательны. Первый аргумент является строкой, содержащей дату и время. Второй аргумент — спецификация формата, используемого функцией strptime() для разбора первого аргумента. Необязательный третий аргумент — строка формата, используемого функцией strftime() для обратного преобразования. Если этот аргумент не указан, применяется строка формата по умолчанию. (Функция setlocale(), используемая в этой программе, рассматривается в разделе 10.4.) Примеры применения этой программы показаны в следующей записи сеанса работы с оболочкой:

$ ./strtime "9:39:46pm 1 Feb 2011" "%I:%M:%S%p %d %b %Y"

calendar time (seconds since Epoch): 1296592786

strftime() yields: 21:39:46 Tuesday, 01 February 2011 CET

Следующий код похож на предыдущий, но на этот раз формат для strftime() указан явным образом:

$ ./strtime "9:39:46pm 1 Feb 2011" "%I:%M:%S%p %d %b %Y" "%F %T"

calendar time (seconds since Epoch): 1296592786

strftime() yields: 2011-02-01 21:39:46


Листинг 10.3. Извлечение и преобразование данных календарного времени

time/strtime.c

#define _XOPEN_SOURCE

#include <time.h>

#include <locale.h>

#include "tlpi_hdr.h"


#define SBUF_SIZE 1000


int

main(int argc, char *argv[])

{

struct tm tm;

char sbuf[SBUF_SIZE];

char *ofmt;


if (argc < 3 || strcmp(argv[1], "-help") == 0)

usageErr("%s input-date-time in-format [out-format]\n", argv[0]);

if (setlocale(LC_ALL, "") == NULL)

errExit("setlocale"); /* Использование настроек локали при преобразовании */


memset(&tm, 0, sizeof(struct tm)); /* Инициализация 'tm' */

if (strptime(argv[1], argv[2], &tm) == NULL)

fatal("strptime");


tm.tm_isdst = -1; /* Не устанавливается функцией strptime(); заставляет функцию

mktime() определить действие режима летнего времени */


printf("calendar time (seconds since Epoch): %ld\n", (long) mktime(&tm));


ofmt = (argc > 3)? argv[3]: "%H:%M:%S %A, %d %B %Y %Z";

if (strftime(sbuf, SBUF_SIZE, ofmt, &tm) == 0)

fatal("strftime returned 0");

printf("strftime() yields: %s\n", sbuf);


exit(EXIT_SUCCESS);

}

time/strtime.c


10.3. Часовые пояса

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


Определение часовых поясов

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

Эти файлы находятся в каталоге /usr/share/zoneinfo. Каждый файл в нем содержит информацию о часовом поясе конкретной страны или региона. Файлы названы в соответствии с тем часовым поясом, описание которого в них дается, поэтому там можно найти файлы с такими именами, как EST (US Eastern Standard Time — североамериканское восточное время), CET (Central European Time — центральноевропейское время), UTC, Turkey и Iran. Кроме того, для создания иерархии групп, связанных с часовыми поясами, могут использоваться подкаталоги. Например, в каталоге Pacific можно найти файлы Auckland, Port_Moresby и Galapagos. Когда мы указываем программе, какой именно часовой пояс использовать, на самом деле указывается относительное путевое имя для одного из файлов часового пояса в этом каталоге.

Местное время для системы определяется файлом часового пояса /etc/localtime, который часто ссылается на один из файлов в каталоге /usr/share/zoneinfo.

Формат файлов часовых поясов задокументирован на странице руководства tzfile(5). Файлы часовых поясов создаются с помощью zic(8), компилятора информации о часовых поясах. С помощью команды zdump можно вывести текущее время для указанных файлов часовых поясов.


Указание часового пояса для программы

Чтобы указать часовой пояс при выполнении программы, переменной среды TZ присваивается значение в виде строки, содержащей символ двоеточия (:), за которым следует одно из названий часовых поясов, определенное в /usr/share/zoneinfo. Установка часового пояса автоматически влияет на функции ctime(), localtime(), mktime() и strftime().

Для получения текущей установки часового пояса в каждой из этих функций применяется функция tzset(3), которая инициализирует три глобальные переменные:

char *tzname[2]; /* Название часового пояса и альтернативного часового пояса

с учетом действия режима летнего времени */

int daylight; /* Ненулевое значение при наличии альтернативного

часового пояса с учетом действия режима летнего времени */

long timezone; /* Разница в секундах между UTC и местным [поясным] временем */

Функция tzset() сначала проверяет значение переменной среды TZ. Если значение для нее не установлено, часовой пояс инициализируется значением по умолчанию, определенным в файле часового пояса /etc/localtime. Если переменная TZ определена и имеет значение, которое не может соответствовать файлу часового пояса, или если оно представляет собой пустую строку, тогда используется UTC. Для переменной среды TZDIR (нестандартное GNU-расширение) может быть установлено имя каталога, в котором требуется вести поиск информации о часовом поясе вместо исходного каталога /usr/share/zoneinfo.

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

$ ./show_time

ctime() of time() value is: Tue Feb 1 10:25:56 2011

asctime() of local time is: Tue Feb 1 10:25:56 2011

strftime() of local time is: Tuesday, 01 Feb 2011, 10:25:56 CET

$ TZ=":Pacific/Auckland"./show_time

ctime() of time() value is: Tue Feb 1 22:26:19 2011

asctime() of local time is: Tue Feb 1 22:26:19 2011

strftime() of local time is: Tuesday, 01 February 2011, 22:26:19 NZDT


Листинг 10.4. Демонстрация эффекта часовых поясов и локалей

time/show_time.c

#include <time.h>

#include <locale.h>

#include "tlpi_hdr.h"


#define BUF_SIZE 200


int

main(int argc, char *argv[])

{

time_t t;

struct tm *loc;

char buf[BUF_SIZE];


if (setlocale(LC_ALL, "") == NULL)

errExit("setlocale"); /* Использование в преобразовании настроек локали */

t = time(NULL);


printf("ctime() of time() value is: %s", ctime(&t));

loc = localtime(&t);

if (loc == NULL)

errExit("localtime");


printf("asctime() of local time is: %s", asctime(loc));

if (strftime(buf, BUF_SIZE, "%A, %d %B %Y, %H:%M:%S %Z", loc) == 0)

fatal("strftime returned 0");

printf("strftime() of local time is: %s\n", buf);

exit(EXIT_SUCCESS);

}

time/show_time.c

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

Еще один метод установки значения для TZ полностью указан в SUSv3. Согласно ему, переменной TZ присваивается срока следующего вида:

std offset [dst [offset][, start-date [/time], end-date [/time]]]

Пробелы включены в показанную выше строку для удобства чтения, но в значении TZ их быть не должно. Квадратные скобки ([]) используются для обозначения необязательных компонентов. Компоненты std и dst — это строки, показывающие стандартный часовой пояс и часовой пояс с учетом действия режима летнего времени; например CET и CEST для центральноевропейского времени и центральноевропейского летнего времени. Смещение offset в каждом случае указывается положительным или отрицательным корректировочным значением, которое прибавляется к местному времени для его преобразования во время UTC. Последние четыре компонента предоставляют правило, описывающее период перехода со стандартного на летнее время.

Даты могут указываться в разных формах, одной из которых является Mm.n.d. Эта запись означает день d (0 = воскресенье, 6 = суббота) недели n (от 1 до 5, где 5 всегда означает последний d день) месяца m (от 1 до 12). Если время опущено, его значение в любом случае устанавливается по умолчанию на 02:00:00 (2 AM).

Определить TZ для Центральной Европы, где стандартное время на час опережает UTC и режим летнего времени (DST) вводится с последнего воскресенья марта до последнего воскресенья октября, а местное время опережает UTC на два часа, можно следующим образом:

TZ="CET-1:00:0 °CEST-2:00:00,M3.5.0,M10.5.0"

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

TZ=":Europe/Berlin"


10.4. Локали

В мире говорят на нескольких тысячах языков, существенная часть которых постоянно используется в компьютерных системах. Кроме того, в разных странах есть разные соглашения для отображения такой информации, как числа, денежные суммы, даты и показания времени. Например, в большинстве европейских стран для отделения целой части от дробной в действительных числах используется запятая, а не точка и в большинстве стран используются форматы для записи дат, отличающиеся от формата MM/DD/YY, принятого в США. В SUSv3 локаль характеризуется как «подмножество переменных пользовательской среды, которые зависят от языковых и культурных норм».

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

Термин «интернационализация» (internationalization) часто записывается в виде i18N, то есть в виде I плюс 18 букв плюс N. Кроме того, что в таком виде это слово записывается быстрее, данная запись устраняет различия в его написании, существующие в английском и американском вариантах английского языка.


Определения локали

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

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

language[_territory[.codeset]][@modifier]

В качестве language используется двухбуквенный код языка по стандарту ISO, а в качестве territory — двухбуквенный код страны по стандарту ISO. Компонент codeset обозначает кодировку символов. Компонент modifier предоставляет средства, позволяющие отличить друг от друга несколько каталогов с локалями, чьи языки, территории и кодировки символов совпадают. Примером полного имени каталога с локалями может служить de_DE.utf-8@euro, которое соответствует следующим региональным настройкам: немецкий язык, Германия, кодировка символов UTF-8, в качестве денежного знака используется евро.

Квадратные скобки в формате наименования каталога показывают, что некоторые части названия каталога локали могут быть опущены. Зачастую название состоит просто из языка (language) и страны (territory). Следовательно, каталог en_US является каталогом локали для англоговорящих Соединенных Штатов, а fr_CH — каталогом локали для франкоговорящего региона Швейцарии.

CH означает Confoederatio Helvetica, латинское (и в силу этого нейтрального по языку для данной местности) название Швейцарии. Имея четыре официальных национальных языка, Швейцария в плане региональных настроек аналогична стране с несколькими часовыми поясами.

Когда в программе указывается, какую именно локаль использовать, мы, по сути, определяем название одного из подкаталогов, находящихся в каталоге /usr/share/locale. Если локаль, определенная в программе, не соответствует в точности названию каталога локали, библиотека языка C ведет поиск соответствия путем разбора компонентов из заданной локали в следующем порядке.

1. Кодировка символов (codeset).

2. Нормализованная кодировка символов (normalized codeset).

3. Страна (territory).

4. Модификатор (modifier).

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

Например, если для программы локаль запрошена как fr_CH.utf-8, но каталога локали под таким названием не существует, то для такой локали подойдет каталог fr_CH, если таковой обнаружится. Если каталога с названием fr_CH не будет, то будет использован каталог локали fr. В маловероятном случае отсутствия каталога fr функция setlocale(), которая вскоре будет рассмотрена, сообщит об ошибке.

Альтернативные способы указания локали для программы определяются в файле /usr/share/locale/locale.alias. Подробности можно найти на странице руководства locale.aliases(5).

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

• В файле LC_COLLATE устанавливается набор правил, описывающих порядок следования символов в их наборе (то есть «алфавитный» порядок для набора символов). Эти правила определяют работу функций strcoll(3) и strxfrm(3). Даже языки, основанные на латинице, не следуют одним и тем же правилам сортировки. Например, в ряде европейских языков имеются дополнительные буквы, которые иногда при сортировке могут следовать за буквой Z. К другим особым случаям можно отнести испанскую двухбуквенную последовательность ll, которая сортируется как одна буква, следующая за буквой l, и немецкие символы умлаутов, такие как «д», которая соответствует сочетанию ae и сортируется как эти две буквы.

• Каталог LC_MESSAGES является одним шагом по направлению к интернационализации сообщений, выводимых программой. Расширенная интернационализация сообщений программы может быть выполнена путем использования либо каталогов сообщений (см. страницы руководства catopen(3) и catgets(3)), либо GNU API gettext (доступного по адресу http://www.gnu.org/).

В версии glibc под номером 2.2.2 введено несколько новых, нестандартных категорий локали. В LC_ADDRESS определяются правила зависящих от локали представлений почтовых адресов. В LC_IDENTIFICATION указывается информация, идентифицирующая локаль. В LC_MEASUREMENT определяется местная система мер (например, метрическая или дюймовая). В LC_NAME устанавливаются местные правила представления личных имен и титулов. В LC_PAPER определяется стандартный для данной местности размер бумаги (например, принятый в США формат Letter или формат A4, используемый в большинстве других стран). В LC_TELEPHONE задаются правила для местного представления внутренних и международных телефонных номеров, а также международного префикса страны и префикса выхода на международную телефонную сеть.


Таблица 10.2. Содержимое подкаталогов локали

Имя файла — Назначение

LC_CTYPE — Файл содержит классификацию символов (см. isalpha(3)) и правила, применяемые при преобразовании регистра

LC_COLLATE — Файл включает правила сортировки набора символов

LC_MONETARY — Файл содержит правила форматирования денежных величин (см. localeconv(3) и <locale.h>)

LC_NUMERIC — Файл содержит правила форматирования для чисел, не являющихся денежными величинами (см. localeconv(3) и <locale.h>)

LC_TIME — Файл включает правила форматирования для даты и времени

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


Фактические настройки локали, определенные в системе, могут изменяться. В SUSv3 насчет этого не выдвигается никаких требований, за исключением необходимости определения стандартной настройки локали по имени POSIX (и по историческим причинам ее синонима по имени C). Эта локаль воспроизводит исторически сложившееся поведение систем UNIX. Так, она основана на наборе кодировки символов ASCII и использует английский язык для названия дней и месяцев, а также односложных ответов yes и no. Денежные и числовые компоненты в этой локалии не определяются.

Команда locale выводит информацию о текущей локали среды (в оболочке). Команда locale — a выводит списком полный набор локалей, определенных в системе.


Задание для программы локали

Для установки и запроса локали программы используется функция setlocale(), синтаксис которой представлен далее.

#include <locale.h>


char *setlocale(int category, const char *locale);

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

Аргумент category выбирает, какую часть данных о локали установить или запросить, и указывается набор констант, чьи имена совпадают с категориями локали, перечисленными в табл. 10.2. Это, к примеру, означает, что можно настроить локаль так, чтобы отображалось время как в Германии, а вместе с тем задать отображение денежных сумм в долларах США. Или же, что бывает значительно чаще, мы можем использовать значение LC_ALL, чтобы указать, что нам нужно установить все аспекты локали.

Есть два различных метода настройки локали с помощью функции setlocale(). Аргумент locale должен быть строкой, указывающей на одну из локалей, определяемых в системе (то есть на имя одного из подкаталогов в каталоге /usr/lib/locale), например de_DE или en_US. Или же locale может быть указан в виде пустой строки, что означает необходимость получения настроек локали из переменных среды:

setlocale(LC_ALL, "");

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

При запуске программы, выполняющей вызов setlocale(LC_ALL,""), мы можем управлять различными аспектами локали, используя набор переменных среды, чьи имена также соответствуют категориям, перечисленным в табл. 10.2: LC_CTYPE, LC_COLLATE, LC_MONETARY, LC_NUMERIC, LC_TIME и LC_MESSAGES. Для указания настроек всей локали также можно воспользоваться переменной среды LC_ALL или LANG. При установке более одной из ранее перечисленных переменных среды у LC_ALL имеется приоритет над всеми другими переменными вида LC_*, а LANG имеет самый низкий уровень приоритета. Следовательно, LANG можно применять для настроек локали, используемых по умолчанию для всех категорий, а затем воспользоваться отдельными переменными LC_* для настройки составляющих локали на что-либо другое, чем эти установки по умолчанию.

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

Настройки локализации управляют работой множества разнообразных GNU/Linux-утилит, а также многих функций в библиотеке glibc. Среди них функции strftime() и strptime() (см. подраздел 10.2.3), о чем свидетельствуют результаты, полученные от strftime() при выполнении программы из листинга 10.4:

$ LANG=de_DE./show_time Немецкая локаль

ctime() of time() value is: Tue Feb 1 12:23:39 2011

asctime() of local time is: Tue Feb 1 12:23:39 2011

strftime() of local time is: Dienstag, 01 Februar 2011, 12:23:39 CET

Следующий код демонстрирует, что LC_TIME имеет преимущество перед LANG:

$ LANG=de_DE LC_TIME=it_IT./show_time Немецкая и итальянская локали

ctime() of time() value is: Tue Feb 1 12:24:03 2011

asctime() of local time is: Tue Feb 1 12:24:03 2011

strftime() of local time is: martedì, 01 febbraio 2011, 12:24:03 CET

А этот код показывает, что LC_ALL имеет преимущество перед LC_TIME:

$ LC_ALL=fr_FR LC_TIME=en_US./show_time Французская и американская (США) локали

ctime() of time() value is: Tue Feb 1 12:25:38 2011

asctime() of local time is: Tue Feb 1 12:25:38 2011

strftime() of local time is: mardi, 01 février 2011, 12:25:38 CET


10.5. Обновление системных часов

Теперь рассмотрим два интерфейса, обновляющих системные часы: settimeofday() и adjtime(). Прикладными программами они используются довольно редко (поскольку обычно системное время поддерживается с помощью средств вроде демона сервиса точного времени Network Time Protocol), и к тому же им нужно, чтобы вызывающий процесс был привилегированным (CAP_SYS_TIME).

Системный вызов settimeofday() является обратным функции gettimeofday() (рассмотренной в разделе 10.1): он присваивает календарному времени системы значение, соответствующее количеству секунд и микросекунд, заданное в структуре timeval, указатель на которую находится в аргументе tv.

#define _BSD_SOURCE

#include <sys/time.h>


int settimeofday(const struct timeval *tv, const struct timezone *tz);

Возвращает при успешном завершении 0 или –1 при ошибке

Как и в случае с gettimeofday(), использование аргумента tz утратило актуальность, и в качестве его значения нужно указывать NULL.

Точность до микросекунд в поле v.tv_usec не означает наличие такой же точности в управлении системными часами, поскольку точность у часов может быть ниже одной микросекунды.

Хотя системный вызов settimeofday() в SUSv3 не определен, он широко доступен во многих других реализациях UNIX.

В Linux также предоставляется системный вызов stime(), предназначенный для установки системных часов. Разница между settimeofday() и stime() состоит в том, что последний вызов позволяет установить новое календарное время с точностью всего лишь в одну секунду. Как и в случае с time() и gettimeofday(), причина существования как stime(), так и settimeofday() имеет исторические корни: последний, задающий более точное значение вызов был добавлен в версии 4.2BSD.

Резкие изменения системного времени, связанные с вызовами settimeofday(), могут плохо влиять на приложения (например, на make(1), систему управления базами данных, использующую метки времени, или на файлы журналов, использующие метки времени). Поэтому при внесении незначительных изменений в установки времени (в пределах нескольких секунд) практически всегда предпочтительнее задействовать библиотечную функцию adjtime(), заставляющую системные часы постепенно выйти на требуемое значение.

#define _BSD_SOURCE

#include <sys/time.h>


int adjtime(struct timeval *delta, struct timeval *olddelta);

Возвращает при успешном завершении 0 или –1 при ошибке

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

Скорость изменения в Linux/x86-32 составляет одну секунду за каждые 2000 секунд (или 43,2 секунды за день).

Может получиться так, что вызов функции adjtime() придется на момент, когда предыдущее изменение показания часов не завершилось. В таком случае объем оставшегося неизмененного времени возвращается в timeval-структуру olddelta. Если это значение нас не интересует, для аргумента olddelta нужно указать NULL. И наоборот, если нас интересуют только сведения о текущем объеме невыполненной коррекции времени и мы не намереваемся изменять значение, в качестве аргумента delta можно указать NULL.

Несмотря на то что в SUSv3 функция adjtime() не указана, она доступна в большинстве реализаций UNIX.

В Linux функция adjtime() реализована в качестве надстройки над более универсальным (и сложным) характерным для Linux системным вызовом adjtimex(). Этот системный вызов используется резидентной про