Linux. Системное программирование [Роберт Лав] (pdf) читать онлайн

-  Linux. Системное программирование  [2-е издание] (и.с. Бестселлеры o’reilly) 11.93 Мб, 448с. скачать: (pdf) - (pdf+fbd)  читать: (полностью) - (постранично) - Роберт Лав

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

ББК 32.973.2-018.2
УДК 004.451
Л13

Лав Р.
Л13

Linux. Системное программирование. 2-е изд. — СПб.: Питер, 2014. — 448 с.:
ил. — (Серия «Бестселлеры O’Reilly»).
ISBN 978-5-496-00747-4
Роберт Лав стоит у истоков создания операционной системы Linux. Он внес существенный вклад
в создание ядра Linux и настольной среды GNOME.
Эта книга представляет собой руководство по системному программированию для Linux, справочник по системным вызовам Linux, а также подробный рассказ о том, как писать более быстрый
и умный код. Роберт Лав четко разграничивает стандартные функции POSIX и специальные службы,
которые предлагаются лишь в Linux. Во втором издании вы изучите эту операционную систему как
с теоретической, так и с прикладной точки зрения.

12+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.2-018.2
УДК 004.451

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

ISBN 978-1449339531 англ.

ISBN 978-5-496-00747-4

Authorized Russian translation of the English edition of Linux System
Programming: Talking Directly to the Kernel and C Library 2nd edition
(ISBN 9781449339531) © 2013 Robert Love.
This translation is published and sold by permission of O’Reilly Media,
Inc., which owns or controls all rights to publish and sell the same
© Перевод на русский язык ООО Издательство «Питер», 2014
© Издание на русском языке, оформление ООО Издательство
«Питер», 2014

Краткое содержание
Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Вступление. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Благодарности. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
От издательства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Глава 1. Введение и основополагающие концепции. . . . . . . . . . . . . . . . . . . . . . . 27
Глава 2. Файловый ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Глава 3. Буферизованный ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Глава 4. Расширенный файловый ввод-вывод . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Глава 5. Управление процессами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
Глава 6. Расширенное управление процессами . . . . . . . . . . . . . . . . . . . . . . . . . 210
Глава 7. Поточность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
Глава 8. Управление файлами и каталогами . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Глава 9. Управление памятью. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Глава 10. Сигналы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Глава 11. Время. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
Приложение A. Расширения GCC для языка C. . . . . . . . . . . . . . . . . . . . . . . . . 427
Приложение Б. Библиография. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441

Оглавление
Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Вступление. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Целевая аудитория и необходимые
Краткое содержание. . . . . . . . . . . .
Версии, рассмотренные в книге. . . .
Условные обозначения . . . . . . . . . .
Работа с примерами кода . . . . . . . .

предпосылки. .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

19
20
21
22
24

Благодарности. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
От издательства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Глава 1. Введение и основополагающие концепции. . . . . . . . . . . . . . . . . . . . . . . 27
Системное программирование. . . . . . . . . . . . . . . . . . . . . . . .
Зачем изучать системное программирование. . . . . . .
Краеугольные камни системного программирования .
Системные вызовы. . . . . . . . . . . . . . . . . . . . . . . . . . .
Библиотека C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Компилятор C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
API и ABI. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ABI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Стандарты. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
История POSIX и SUS. . . . . . . . . . . . . . . . . . . . . . . . .
Стандарты языка C . . . . . . . . . . . . . . . . . . . . . . . . . .
Linux и стандарты . . . . . . . . . . . . . . . . . . . . . . . . . . .
Стандарты и эта книга. . . . . . . . . . . . . . . . . . . . . . . .
Концепции программирования в Linux. . . . . . . . . . . . . . . . . .
Файлы и файловая система. . . . . . . . . . . . . . . . . . . .
Процессы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пользователи и группы. . . . . . . . . . . . . . . . . . . . . . .
Права доступа. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

27
28
29
29
30
31
31
32
32
33
34
34
35
36
37
37
45
47
48

7

Оглавление

Сигналы . . . . . . . . . . . . . . . . . . . . . . . . . . .
Межпроцессное взаимодействие . . . . . . . . .
Заголовки. . . . . . . . . . . . . . . . . . . . . . . . . .
Обработка ошибок. . . . . . . . . . . . . . . . . . . .
Добро пожаловать в системное программирование.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

49
50
50
50
53

Глава 2. Файловый ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Открытие файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Системный вызов open() . . . . . . . . . . . . . . . . . . .
Владельцы новых файлов . . . . . . . . . . . . . . . . . .
Права доступа новых файлов. . . . . . . . . . . . . . . .
Функция creat(). . . . . . . . . . . . . . . . . . . . . . . . . .
Возвращаемые значения и коды ошибок . . . . . . .
Считывание с помощью read(). . . . . . . . . . . . . . . . . . . . .
Возвращаемые значения. . . . . . . . . . . . . . . . . . .
Считывание всех байтов . . . . . . . . . . . . . . . . . . .
Неблокирующее считывание . . . . . . . . . . . . . . . .
Другие значения ошибок. . . . . . . . . . . . . . . . . . .
Ограничения размера для read() . . . . . . . . . . . . .
Запись с помощью write() . . . . . . . . . . . . . . . . . . . . . . . .
Случаи частичной записи. . . . . . . . . . . . . . . . . . .
Режим дозаписи. . . . . . . . . . . . . . . . . . . . . . . . . .
Неблокирующая запись. . . . . . . . . . . . . . . . . . . .
Другие коды ошибок. . . . . . . . . . . . . . . . . . . . . .
Ограничения размера при использовании write().
Поведение write() . . . . . . . . . . . . . . . . . . . . . . . .
Синхронизированный ввод-вывод. . . . . . . . . . . . . . . . . .
fsync() и fdatasync(). . . . . . . . . . . . . . . . . . . . . . .
sync() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Флаг O_SYNC. . . . . . . . . . . . . . . . . . . . . . . . . . . .
Флаги O_DSYNC и O_RSYNC. . . . . . . . . . . . . . . . .
Непосредственный ввод-вывод. . . . . . . . . . . . . . . . . . . .
Закрытие файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Значения ошибок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Позиционирование с помощью Iseek(). . . . . . . . . . . . . . .
Поиск с выходом за пределы файла. . . . . . . . . . .
Ограничения. . . . . . . . . . . . . . . . . . . . . . . . . . . .
Позиционное чтение и запись. . . . . . . . . . . . . . . . . . . . .
Усечение файлов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

55
55
58
58
60
61
61
62
63
64
64
65
65
66
67
67
68
68
68
70
70
72
73
74
74
75
76
76
78
79
79
80

8

Оглавление

Мультиплексный ввод-вывод. . . . . . . . . .
select() . . . . . . . . . . . . . . . . . . . .
Системный вызов poll() . . . . . . . .
Сравнение poll() и select() . . . . . .
Внутренняя организация ядра. . . . . . . . .
Виртуальная файловая система. .
Страничный кэш . . . . . . . . . . . . .
Страничная отложенная запись. .
Резюме. . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

81
83
88
92
93
93
94
96
98

Глава 3. Буферизованный ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Ввод-вывод с пользовательским буфером . . . . . . . . . . . . . . . . . . . . . . . . . . .
Стандартный ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Открытие файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Открытие потока данных с помощью файлового дескриптора . . . . . . . . . . . .
Закрытие потоков данных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Считывание из потока данных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Считывание одного символа в момент времени . . . . . . . . . . . . . . . . .
Считывание целой строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Считывание двоичных данных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Запись в поток данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Запись отдельного символа. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Запись строки символов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Запись двоичных данных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример программы, в которой используется буферизованный
ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Позиционирование в потоке данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Сброс потока данных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ошибки и конец файла. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Получение ассоциированного файлового дескриптора. . . . . . . . . . . . . . . . . .
Управление буферизацией . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Безопасность программных потоков. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Блокировка файлов вручную. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Неблокируемые потоковые операции. . . . . . . . . . . . . . . . . . . . . . . . .
Недостатки стандартного ввода-вывода. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Резюме. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

100
102
103
104
105
106
106
107
109
110
110
111
111
112
113
115
116
117
117
.119
120
122
123
123

Глава 4. Расширенный файловый ввод-вывод . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Фрагментированный ввод-вывод. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Системные вызовы readv() и writev(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

Оглавление

9

Возвращаемые значения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Пример использования writev() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
Пример использования readv(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Реализация. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Опрос событий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Создание нового экземпляра epoll. . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Управление epoll. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Ожидание событий с помощью epoll. . . . . . . . . . . . . . . . . . . . . . . . . . 135
Сравнение событий, запускаемых по фронту и по уровню сигнала. . . 136
Отображение файлов в память . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
mmap(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Системный вызов munmap(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Пример отображения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Преимущества mmap(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Недостатки mmap(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Изменение размеров отображения. . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Изменение защиты отображения. . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Синхронизация файла с помощью отображения. . . . . . . . . . . . . . . . . 147
Извещения об отображении. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Извещения об обычном файловом вводе-выводе. . . . . . . . . . . . . . . . . . . . . . 151
Системный вызов posix_fadvise() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Системный вызов readahead() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Рекомендации почти ничего не стоят. . . . . . . . . . . . . . . . . . . . . . . . . 153
Синхронизированные, синхронные и асинхронные операции. . . . . . . . . . . . . 154
Планировщики и производительность ввода-вывода. . . . . . . . . . . . . . . . . . . 156
Адресация диска. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Жизненный цикл планировщика ввода-вывода. . . . . . . . . . . . . . . . . . 157
Помощь при считывании . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Выбор и настройка планировщика ввода-вывода. . . . . . . . . . . . . . . . 162
Оптимизация производительности ввода-вывода. . . . . . . . . . . . . . . . 163
Резюме. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170

Глава 5. Управление процессами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
Программы, процессы и потоки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
Идентификатор процесса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Выделение идентификатора процесса . . . . . . . . . . . . . . . . . . . . . . . . 173
Иерархия процессов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
pid_t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Получение идентификаторов процесса и родительского процесса . . . 174

10

Оглавление

Запуск нового процесса. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Семейство вызовов exec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Системные вызовы fork(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Завершение процесса. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Другие способы завершения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
atexit(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
on_exit() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
SIGCHLD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Ожидание завершенных дочерних процессов. . . . . . . . . . . . . . . . . . . . . . . . . 185
Ожидание определенного процесса. . . . . . . . . . . . . . . . . . . . . . . . . . 188
Еще больше гибкости при ожидании . . . . . . . . . . . . . . . . . . . . . . . . . 190
На сцену выходит BSD: wait3() и wait4(). . . . . . . . . . . . . . . . . . . . . . . 192
Запуск и ожидание нового процесса. . . . . . . . . . . . . . . . . . . . . . . . . .193
Зомби . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Пользователи и группы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Реальные, действительные и сохраненные идентификаторы
пользователя и группы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Изменение реального или сохраненного идентификатора
пользователя или группы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .198
Изменение действительного идентификатора пользователя
или группы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Изменение идентификаторов пользователя и группы согласно
стилю BSD. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Изменение идентификаторов пользователя и группы согласно
стилю HP-UX. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
Действия с предпочтительными идентификаторами пользователя
или группы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Поддержка сохраненных пользовательских идентификаторов . . . . . . 201
Получение идентификаторов пользователя и группы. . . . . . . . . . . . . 201
Сессии и группы процессов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Системные вызовы сессий. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
Системные вызовы групп процессов. . . . . . . . . . . . . . . . . . . . . . . . . . 205
Устаревшие функции для группы процессов. . . . . . . . . . . . . . . . . . . . 206
Демоны. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Резюме. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209

Глава 6. Расширенное управление процессами . . . . . . . . . . . . . . . . . . . . . . . . . 210
Планирование процессов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Кванты времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Процессы ввода-вывода против ограниченных процессором. . . . . . . . 212

11

Оглавление

Приоритетное планирование. . . . . . . . . . . . . . . . . . . . . . . . . . . .
Completely Fair Scheduler. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Высвобождение ресурсов процессора . . . . . . . . . . . . . . . . . . . . . . . . . . .
Приоритеты процессов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
nice(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
getpriority() и setpriority(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Приоритеты ввода-вывода. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Привязка процессов к процессору. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Системы реального времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Мягкие и жесткие системы реального времени. . . . . . . . . . . . . . .
Задержка, колебание и временное ограничение. . . . . . . . . . . . . .
Поддержка реального времени в Linux. . . . . . . . . . . . . . . . . . . . .
Политики планирования и приоритеты в Linux. . . . . . . . . . . . . . .
Установка параметров планирования. . . . . . . . . . . . . . . . . . . . . .
sched_rr_get_interval(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Предосторожности при работе с процессами реального времени.
Детерминизм. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Лимиты ресурсов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Лимиты по умолчанию. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Установка и проверка лимитов. . . . . . . . . . . . . . . . . . . . . . . . . . .
Коды ошибок. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

213
213
215
216
217
218
219
220
223
224
225
226
226
230
233
235
235
238
242
243
244

Глава 7. Поточность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
Бинарные модули, процессы и потоки. . . . . .
Многопоточность. . . . . . . . . . . . . . . . . . . . . .
Издержки многопоточности. . . . . . . .
Альтернативы многопоточности. . . . .
Поточные модели . . . . . . . . . . . . . . . . . . . . .
Поточность на уровне пользователя .
Комбинированная поточность . . . . . .
Сопрограммы и фиберы. . . . . . . . . . .
Шаблоны поточности . . . . . . . . . . . . . . . . . .
Поток на соединение. . . . . . . . . . . . .
Поток, управляемый событием. . . . . .
Конкурентность, параллелизм и гонки. . . . . .
Синхронизация. . . . . . . . . . . . . . . . . . . . . . .
Мьютексы . . . . . . . . . . . . . . . . . . . . .
Взаимные блокировки. . . . . . . . . . . .
Р-потоки. . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

245
246
248
248
249
249
250
251
251
251
252
253
256
257
258
260

12
Реализация поточности в Linux . . . .
API для работы с Р-потоками. . . . . .
Связывание Р-потоков. . . . . . . . . . . . . . . . .
Создание потоков. . . . . . . . . . . . . . . . . . . .
Идентификаторы потоков . . . . . . . . . . . . . .
Завершение потоков. . . . . . . . . . . . . . . . . .
Самозавершение. . . . . . . . . . . . . . .
Завершение других потоков. . . . . . .
Присоединение и отсоединение потоков. . .
Присоединение потоков. . . . . . . . . .
Отсоединение потоков. . . . . . . . . . .
Пример поточности. . . . . . . . . . . . . . . . . . .
Мьютексы Р-потоков. . . . . . . . . . . . . . . . . .
Инициализация мьютексов. . . . . . . .
Запирание мьютексов . . . . . . . . . . .
Отпирание мьютексов. . . . . . . . . . .
Пример использования мьютексов. .
Дальнейшее изучение. . . . . . . . . . . . . . . . .

Оглавление

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

261
261
262
262
264
265
265
266
268
268
269
269
270
270
271
271
272
273

Глава 8. Управление файлами и каталогами . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Файлы и их метаданные . . . . . . . . . . . . . . . . . . . . . . .
Семейство stat. . . . . . . . . . . . . . . . . . . . . . . . .
Разрешения. . . . . . . . . . . . . . . . . . . . . . . . . . .
Владение. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Расширенные атрибуты. . . . . . . . . . . . . . . . . .
Перечисление расширенных атрибутов файла.
Каталоги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Текущий рабочий каталог . . . . . . . . . . . . . . . .
Создание каталогов. . . . . . . . . . . . . . . . . . . . .
Удаление каталогов. . . . . . . . . . . . . . . . . . . . .
Чтение содержимого каталога. . . . . . . . . . . . .
Ссылки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Жесткие ссылки. . . . . . . . . . . . . . . . . . . . . . . .
Символические ссылки. . . . . . . . . . . . . . . . . .
Удаление ссылки. . . . . . . . . . . . . . . . . . . . . . .
Копирование и перемещение файлов . . . . . . . . . . . . .
Копирование. . . . . . . . . . . . . . . . . . . . . . . . . .
Перемещение . . . . . . . . . . . . . . . . . . . . . . . . .
Узлы устройств. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
.................
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .

275
276
280
281
284
289
292
293
298
299
300
303
304
.305
307
308
308
309
311

13

Оглавление

Специальные узлы устройств. . . . . . . .
Генератор случайных чисел. . . . . . . . .
Внеполосное взаимодействие . . . . . . . . . . . . .
Отслеживание файловых событий. . . . . . . . . .
Инициализация inotify . . . . . . . . . . . . .
Стражи . . . . . . . . . . . . . . . . . . . . . . . .
События inotify. . . . . . . . . . . . . . . . . .
Расширенные события отслеживания. .
Удаление стража inotify. . . . . . . . . . . .
Получение размера очереди событий. .
Уничтожение экземпляра inotify. . . . . .

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
....................
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .

311
312
312
314
315
316
.317
321
321
322
323

Глава 9. Управление памятью. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Адресное пространство процесса. . . . . . . . . . . . . . . .
Страницы и их подкачка . . . . . . . . . . . . . . . .
Области памяти. . . . . . . . . . . . . . . . . . . . . . .
Выделение динамической памяти . . . . . . . . . . . . . . .
Выделение массивов. . . . . . . . . . . . . . . . . . .
Изменение размера выделенных областей. . .
Освобождение динамической памяти. . . . . . .
Выравнивание. . . . . . . . . . . . . . . . . . . . . . . .
Управление сегментом данных . . . . . . . . . . . . . . . . .
Анонимные отображения в памяти . . . . . . . . . . . . . .
Создание анонимных отображений в памяти .
Отображение /dev/zero . . . . . . . . . . . . . . . . .
Расширенное выделение памяти. . . . . . . . . . . . . . . .
Отладка при операциях выделения памяти . . . . . . . .
Выделение памяти на основе стека. . . . . . . . . . . . . .
Дублирование строк в стеке. . . . . . . . . . . . .
Массивы переменной длины . . . . . . . . . . . . .
Выбор механизма выделения памяти. . . . . . . . . . . . .
Управление памятью. . . . . . . . . . . . . . . . . . . . . . . . .
Установка байтов. . . . . . . . . . . . . . . . . . . . . .
Сравнение байтов. . . . . . . . . . . . . . . . . . . . .
Перемещение байтов. . . . . . . . . . . . . . . . . . .
Поиск байтов. . . . . . . . . . . . . . . . . . . . . . . . .
Перещелкивание байтов . . . . . . . . . . . . . . . .
Блокировка памяти. . . . . . . . . . . . . . . . . . . . . . . . . .
Блокировка части адресного пространства. . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
............
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .

324
324
326
327
329
331
332
335
339
340
342
344
345
348
349
.351
352
353
354
354
355
356
357
358
358
359

14

Оглавление

Блокировка всего адресного пространства. . . . . . . . . . . . . . . . . . . . . 360
Разблокировка памяти. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
Лимиты блокировки. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Находится ли страница в физической памяти. . . . . . . . . . . . . . . . . . . 362
Уступающее выделение памяти. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363

Глава 10. Сигналы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Концепции, связанные с сигналами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Идентификаторы сигналов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Сигналы, поддерживаемые в Linux. . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Основы управления сигналами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
Ожидание любого сигнала. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
Примеры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Выполнение и наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Сопоставление номеров сигналов и строк . . . . . . . . . . . . . . . . . . . . . 377
Отправка сигнала. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
Права доступа. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
Примеры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Отправка сигнала самому себе. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Отправка сигнала целой группе процессов. . . . . . . . . . . . . . . . . . . . . 380
Реентерабельность. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
Наборы сигналов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
Блокировка сигналов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
Получение сигналов, ожидающих обработки. . . . . . . . . . . . . . . . . . . 385
Ожидание набора сигналов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
Расширенное управление сигналами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
Структура siginfo_t. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
Удивительный мир si_code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Отправка сигнала с полезной нагрузкой . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
Изъян в UNIX?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393

Глава 11. Время. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
Структуры данных, связанные с представлением времени. . . . . . . . . . . . . . . 397
Оригинальное представление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
А теперь — с микросекундной точностью! . . . . . . . . . . . . . . . . . . . . . 397
И еще лучше: наносекундная точность. . . . . . . . . . . . . . . . . . . . . . . . 398
Разбиение времени. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
Тип для процессного времени. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399

Оглавление

15

Часы POSIX. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Получение текущего времени суток. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
Более удобный интерфейс. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
Продвинутый интерфейс. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Получение процессного времени. . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Установка текущего времени суток . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Установка времени с заданной точностью . . . . . . . . . . . . . . . . . . . . . 405
Продвинутый интерфейс для установки времени. . . . . . . . . . . . . . . . 406
Эксперименты с временем. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
Настройка системных часов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
Засыпание и ожидание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
Засыпание с микросекундной точностью . . . . . . . . . . . . . . . . . . . . . . 412
Засыпание с наносекундной точностью . . . . . . . . . . . . . . . . . . . . . . . 413
Продвинутая работа со спящим режимом. . . . . . . . . . . . . . . . . . . . . . 415
Переносимый способ засыпания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
Превышение пределов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Альтернативы засыпанию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Таймеры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Простые варианты сигнализации. . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Интервальные таймеры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
Функции для расширенной работы с таймерами. . . . . . . . . . . . . . . . . 421

Приложение A. Расширения GCC для языка C. . . . . . . . . . . . . . . . . . . . . . . . . 427
GNU C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427
Встраиваемые функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Подавление встраивания. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Чистые функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Постоянные функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
Невозвращаемые функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
Функции, выделяющие память. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
Принудительная проверка возвращаемого значения вызывающей
стороной. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
Как пометить функцию как устаревшую. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
Как пометить функцию как используемую. . . . . . . . . . . . . . . . . . . . . . . . . . . 431
Как пометить функции или параметры как неиспользуемые. . . . . . . . . . . . . . 432
Упаковка структуры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Увеличение границы выравнивания переменной. . . . . . . . . . . . . . . . . . . . . . 433
Помещение глобальных переменных в регистр . . . . . . . . . . . . . . . . . . . . . . . 434

16

Оглавление

Аннотирование ветвей. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .434
Получение типа выражения. . . . . . . . . . . . . . . . . . . . . . . . . .
Получение границы выравнивания типа . . . . . . . . . . . . . . . .
Смещение члена внутри структуры . . . . . . . . . . . . . . . . . . . .
Получение возвращаемого адреса функции. . . . . . . . . . . . . .
Диапазоны оператора case . . . . . . . . . . . . . . . . . . . . . . . . . .
Арифметика указателей типа void и указателей на функции.
Более переносимо и красиво. . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

. . .
. . .
. . .
. . .
. . .
...
. . .

.
.
.
.
.
.
.

435
436
437
437
438
.438
439

Приложение Б. Библиография. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441
Книги по языку программирования C. . . . . . . .
Книги по программированию в Linux. . . . . . . .
Книги, посвященные ядру Linux. . . . . . . . . . . .
Книги об организации операционных систем. .

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

441
442
443
443

Предисловие
Есть старая шутка, что разработчики ядра Linux, рассердившись, могут в сердцах
бросить: «Все ваше пользовательское пространство — просто тестовая нагрузка
для ядра!»
Говоря такое, разработчики просто пытаются умыть руки и снять с себя ответ­
ственность за любые случаи, в которых ядру не удается обеспечивать максимально
эффективную работу пользовательского кода. По мнению создателей ядра, про­
граммистам, работающим в пользовательском пространстве, следует просто посто­
рониться и исправлять собственный код, ведь проблемы могут объясняться чем
угодно, но не недостатками ядра.
Уже более трех лет назад один из ведущих разработчиков ядра Linux прочи­
тал лекцию под названием «Почему пользовательское пространство — ерунда».
Целью лекции было доказать, что обычно корень проблем лежит не в ядре. Вы­
ступив перед переполненной аудиторией, этот специалист привел примеры от­
вратительного пользовательского кода, на который практически всем пользова­
телям Linux приходится полагаться ежедневно. Другие разработчики ядра создали
специальные инструменты, демонстрирующие, как сильно пользовательские
программы могут злоупотреблять оборудованием и растрачивать заряд ничего
не подозревающего ноутбука.
Однако если пользовательский код и может быть банальной «тестовой нагруз­
кой», которая порой возмущает разработчиков ядра, необходимо признать, что
и сами разработчики ядра ежедневно вынуждены работать с тем самым пользова­
тельским кодом. Если бы его не было, ядро годилось бы для выполнения един­
ственной функции — вывода на экран перемежающихся серий ABABAB.
В настоящее время Linux является наиболее гибкой и мощной из когда-либо
созданных операционных систем. Linux с успехом применяется как в крошечных
сотовых телефонах и внедренных устройствах, так и на 90 % из 500 мощнейших
суперкомпьютеров в мире. Ни одна другая операционная система не обладает та­
кими превосходными возможностями масштабирования и адаптации к нюансам
разнообразных типов оборудования и экосистем.
Однако код, работающий в пользовательском пространстве Linux, способен
взаимодействовать со всеми этими платформами не менее эффективно, чем ядро,
обеспечивая функционирование реальных прикладных приложений и утилит, с ко­
торыми и приходится работать нам с вами.
В своей книге Роберт Лав (Robert Love) поставил перед собой титаническую
задачу — рассказать читателю практически обо всех системных вызовах, проис­
ходящих в Linux. В результате получился настоящий фолиант, позволяющий

18

Предисловие

вам полностью понять, как работает Linux «с пользовательской точки зрения»
и как максимально эффективно использовать всю мощь этой операционной
системы.
Прочтя данную книгу, вы научитесь писать код, который будет работать во
всех дистрибутивах Linux на самом разном оборудовании. Книга поможет вам
осознать, как функционирует Linux и как наиболее эффективно задействовать
ее гибкость.
В конечном итоге эта книга научит вас писать код, который никто уже не назо­
вет ерундой, а это самое главное.
Грег Кроах-Хартман (Greg Kroah-Hartman)

Вступление
Данная книга рассказывает о системном программировании в Linux. Системное
программирование — это практика написания системного ПО, низкоуровневый код
которого взаимодействует непосредственно с ядром и основными системными
библиотеками. Иными словами, речь далее пойдет в основном о системных вызовах
Linux и низкоуровневых функциях, в частности тех, которые определены в биб­
лиотеке C.
Есть немало пособий, посвященных системному программированию для UNIXсистем, но вы почти не найдете таких, которые рассматривают данную тему доста­
точно подробно и фокусируются именно на Linux. Еще меньше подобных книг
учитывают новейшие релизы Linux и продвинутые интерфейсы, ориентированные
исключительно на Linux. Эта книга не только лишена всех перечисленных недос­
татков, но и обладает важным достоинством: дело в том, что я написал массу кода
для Linux, как для ядра, так и для системных программ, расположенных непосред­
ственно «над ядром». На самом деле я реализовал на практике ряд системных
вызовов и других функций, описанных далее. Соответственно книга содержит
богатый материал, рассказывая не только о том, как должны работать системные
интерфейсы, но и о том, как они действительно работают и как вы сможете
использовать их с максимальной эффективностью. Таким образом, данная книга
одновременно является и руководством по системному программированию для
Linux, и справочным пособием, описывающим системные вызовы Linux, и под­
робным повествованием о том, как создавать более интеллектуальный и быстрый
код. Текст написан простым, доступным языком. Независимо от того, является ли
создание системного кода вашей основной работой, эта книга научит полезным
приемам, которые помогут вам стать по-настоящему высокопрофессиональным
программистом.

Целевая аудитория и необходимые
предпосылки
Пособие предназначается для читателей, знакомых с программированием на язы­
ке C и с применяемой в Linux экосистемой программирования. Не обязательно быть
экспертом в этих темах, но в них нужно как минимум ориентироваться. Если вам
не приходилось работать с текстовыми редакторами для UNIX — наиболее извест­
ными и хорошо себя зарекомендовавшими являются Emacs и vim, — поэксперимен­
тируйте с ними. Кроме того, следует в общих чертах представлять работу с gcc, gdb,

20

Вступление

make и др. Существует еще множество инструментов и практикумов по программи­

рованию для Linux; в приложении Б в конце перечислены некоторые полезные
источники.
Кроме того, я ожидаю от читателя определенных знаний в области системного
программирования для Linux и UNIX. Эта книга начинается с самых основ, ее темы
постепенно усложняются вплоть до обсуждения наиболее продвинутых интерфей­
сов и приемов оптимизации. Надеюсь, пособие понравится читателям с самыми
разными уровнями подготовки, научит их чему-то ценному и новому. Пока писал
книгу, я сам узнал немало интересного.
У меня были определенные предположения об убеждениях и мотивации чита­
теля. Инженеры, желающие (более качественно) программировать на системном
уровне, являются основной целевой аудиторией, но книга будет интересна и про­
граммистам, которые специализируются на высокоуровневом коде и желают при­
обрести более солидные базовые знания. Любознательным хакерам пособие также
понравится, утолит их жажду нового. Книга задумывалась так, чтобы заинтересо­
вать большинство программистов.
В любом случае, независимо от ваших мотивов, надеюсь, что чтение окажется
для вас интересным!

Краткое содержание
Книга разделена на 11 глав и 2 приложения.
 Глава 1. Введение и основополагающие концепции. Она — введение в пробле­

му. Здесь делается обзор Linux, системного программирования, ядра, библио­
теки C и компилятора C. Главу следует изучить даже самым опытным пользо­
вателям.
 Глава 2. Файловый ввод-вывод. Тут дается вводная информация о фай­

лах — наиболее важной абстракции в экосистеме UNIX, а также файловом
вводе/выводе, который является основой процесса программирования для
Linux. Подробно рассматриваются считывание информации из файлов и запись
информации в них, а также другие базовые операции файлового ввода-выво­
да. Итоговая часть главы рассказывает, как ядро Linux внедряет (реализует)
концепцию файлов и управляет ими.
 Глава 3. Буферизованный ввод-вывод. Здесь обсуждается проблема, связанная

с базовыми интерфейсами ввода-вывода — управление размером буфера, —
и рассказывается о буферизованном вводе-выводе вообще, а также стандартном
вводе-выводе в частности как о возможных решениях.
 Глава 4. Расширенный файловый ввод-вывод. Завершает трио тем о вводе-

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

Версии, рассмотренные в книге

21

 Глава 5. Управление процессами. В ней читатель познакомится со второй по

важности абстракцией UNIX — процессом — и семейством системных вызовов,
предназначенных для базового управления процессами, в частности древним
феноменом ветвления (fork).
 Глава 6. Расширенное управление процессами. Здесь продолжается обсужде­

ние процессов. Глава начинается с рассмотрения продвинутых способов управ­
ления процессами, в частности управления в реальном времени.
 Глава 7. Поточность. Здесь обсуждаются потоки и многопоточное программи­

рование. Глава посвящена в основном высокоуровневым концепциям проекти­
рования. В частности, в ней читатель познакомится с API многопоточности
POSIX, который называется Pthreads.
 Глава 8. Управление файлами и каталогами. Тут обсуждаются вопросы созда­

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

Глава начинается с ознакомления с основными концепциями UNIX, связанны­
ми с памятью, в частности с адресным пространством процесса и подкачкой
страниц. Далее мы поговорим об интерфейсах, к которым можно обращаться
для получения памяти и через которые можно возвращать память обратно в ядро.
В заключение мы ознакомимся с продвинутыми интерфейсами, предназначен­
ными для управления памятью.
 Глава 10. Сигналы. Здесь рассматриваются сигналы. Глава начинается с обсу­

ждения природы сигналов и их роли в системе UNIX. Затем описываются
сигнальные интерфейсы, от самых простых к наиболее сложным.
 Глава 11. Время. Она посвящена обсуждению времени, спящего режима

и управления часами. Здесь рассмотрены все базовые интерфейсы вплоть до
часов POSIX и таймеров высокого разрешения.
 Приложение А. В нем рассматриваются многие языковые расширения, предо­
ставляемые gcc и GNU C, в частности атрибуты, позволяющие сделать функцию

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

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

Версии, рассмотренные в книге
Системный интерфейс Linux определяется как бинарный (двоичный) интерфейс
приложений и интерфейс программирования приложений, предоставляемый бла­
годаря взаимодействию трех сущностей: ядра Linux (центра операционной систе­
мы), библиотеки GNU C (glibc) и компилятора GNU C (gcc — в настоящее время

22

Вступление

он официально называется набором компиляторов для GNU и применяется для
работы с различными языками, но нас интересует только C). В этой книге рассмот­
рен системный интерфейс, определенный с применением версии ядра Linux 3.9,
версий glibc 2.17 и gcc 4.8. Более новые интерфейсы этих компонентов должны
и далее соответствовать интерфейсам и поведениям, документированным в данной
книге. Аналогично многие интерфейсы, о которых нам предстоит поговорить, дав­
но используются в составе Linux и поэтому обладают обратной совместимостью
с более ранними версиями ядра, glibc и gcc.
Если любую развивающуюся операционную систему можно сравнить со сколь­
зящей мишенью, то Linux — это просто гепард в прыжке. Прогресс измеряется
днями, а не годами, частые релизы ядра и других компонентов постоянно меняют
и правила игры, и само игровое поле. Ни в одной книге не удалось бы сделать до­
статочно долговечный слепок такого динамичного явления.
Тем не менее экосистема, в которой протекает системное программирование,
очень стабильна. Разработчикам ядра приходится проявлять недюжинную изо­
бретательность, чтобы не повредить системные вызовы, разработчики glibc край­
не высоко ценят прямую и обратную совместимость, а цепочка инструментов Linux
(набор программ для написания кода) создает взаимно совместимый код в раз­
личных версиях. Следовательно, при всей динамичности Linux системное про­
граммирование для этой операционной системы остается стабильным. Книга,
представляющая собой «мгновенный снимок» системы, особенно на современном
этапе развития Linux, обладает исключительной фактической долговечностью.
Я пытаюсь сказать: не беспокойтесь, что системные интерфейсы вскоре изменят­
ся, и смело покупайте эту книгу!

Условные обозначения
В книге применяются следующие условные обозначения.
Курсивный шрифт
Им обозначаются новые термины и понятия.
Шрифт для названий
Используется для обозначения URL, адресов электронной почты, а также соче­
таний клавиш и названий элементов интерфейса.
Шрифт для команд

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

Используется в листингах программного кода.
ПРИМЕЧАНИЕ
Данная врезка содержит совет, замечание практического характера или общее замечание.

Условные обозначения

23

ВНИМАНИЕ
Такая врезка содержит какое-либо предостережение.

Большинство примеров кода в книге представляют собой краткие фрагменты,
которые легко можно использовать повторно. Они выглядят примерно так:
while (1) {
int ret;
ret = fork ();
if(ret== –1)
perror("fork");
}

Пришлось проделать огромную работу, чтобы фрагменты кода получились столь
краткими и при этом не утратили практической ценности. Для работы вам не по­
требуется никаких специальных заголовочных файлов, переполненных безумными
макросами и сокращениями, о смысле которых остается только догадываться. Я не
писал нескольких гигантских программ, а ограничился многочисленными, но сжа­
тыми примерами, которые, будучи практическими и наглядными, сделаны макси­
мально компактными и ясными. Надеюсь, при первом прочтении книги они послу­
жат вам удобным пособием, а на последующих этапах работы станут хорошим
справочным материалом.
Почти все примеры являются самодостаточными. Это означает, что вы можете
просто скопировать их в текстовый редактор и смело использовать на практике.
Если не указано иное, сборка всех фрагментов кода должна происходить без при­
менения каких-либо специальных индикаторов компилятора (в отдельных случа­
ях понадобится связь со специальной библиотекой). Рекомендую следующую
команду для компиляции файла исходников:
$ gcc -Wall -Wextra -O2 -g -o snippet snippet.c

Она собирает файл исходного кода snippet.c в исполняемый бинарный файл
snippet, обеспечивая выполнение многих предупреждающих проверок, значитель­
ных, но разумных оптимизаций, а также отладку. Код из книги должен компили­
роваться без возникновения ошибок или предупреждений — хотя, конечно, вам для
начала может потребоваться построить скелетное приложение на базе того или
иного фрагмента кода.
Когда в каком-либо разделе вы знакомитесь с новой функцией, она записывается
в обычном для UNIX формате справочной страницы такого вида:
#include
int posix_fadvise (int fd, off_t pos, off_t len, int advice);

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

24

Вступление

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

Благодарности
Эта книга могла появиться на свет только благодаря участию множества умных
и великодушных людей. Конечно, любой их список будет неполным, но я искрен­
не хотел бы поблагодарить за участие всех, кто помогал мне в работе, воодушевлял
меня, делился знаниями и поддерживал.
Энди Орам (Andy Oram) — феноменальный редактор и удивительный человек.
Работа над книгой никогда бы не завершилась без его упорного труда. Энди отно­
сится к редкому типу людей, которые обладают одновременно и глубокими техни­
ческими знаниями, и поэтическим языковым чутьем.
У этой книги были превосходные технические рецензенты, истинные мастера
своего искусства, без участия которых эта работа была бы бледной тенью того, что
вы сейчас читаете. Техническими рецензентами книги выступили Джереми Эллисон
(Jeremy Allison), Роберт Пи-Джи Дэй (Robert P. J. Day), Кеннет Гейсшет (Kenneth
Geisshirt), Джоуи Шоу (Joey Shaw) и Джеймс Уилкокс (James Willcox). Они славно
поработали, и если в книге и найдутся ошибки, то это только моя вина.
Мои коллеги из Google были и остаются самой умной и самоотверженной ко­
мандой инженеров, вместе с которыми я имел счастье работать. «Покой нам толь­
ко снится» — такой фразой в лучшем ее смысле можно охарактеризовать этот
коллектив. Спасибо вам за проекты в области системного программирования, ко­
торые помогли мне создать этот труд, и за атмосферу, подвигающую человека на
творческие свершения, например написание подобной книги.
По многим причинам я хотел бы выразить благодарность и глубокое уважение
Полу Амичи (Paul Amici), Майки Бэббиту (Mikey Babbitt), Нату Фридману (Nat
Friedman), Мигелю де Иказе (Miguel de Icaza), Грегу Кроаху-Хартману (Greg
Kroah-Hartman), Дорис Лав (Doris Love), Линде Лав (Linda Love), Тиму О’Рейли
(Tim O’Reilly), Сальваторе Рибаудо (Salvatore Ribaudo) и его семье, Крису Ривере
(Chris Rivera), Кэролин Родон (Carolyn Rodon), Саре Стюарт (Sarah Stewart),
Питеру Тейчману (Peter Teichman), Линусу Торвальдсу (Linus Torvalds), Джону
Троубриджу (Jon Trowbridge), Джереми ван Дорену (Jeremy van Doren) и его семье,
Луису Вилье (Luis Villa), Стиву Вайсбергу (Steve Weisberg) и его семье, а также
Хелен Уиснант (Helen Whisnant).
Наконец, благодарю моих родителей — Боба (Bob) и Элен (Elaine).
Роберт Лав, Бостон

От издательства
Ваши замечания, предложения и вопросы отправляйте по адресу электронной
почты vinitski@minsk.piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На сайте издательства http://www.piter.com вы найдете подробную информацию
о наших книгах.

Введение
и ОСНОВОl10лагающие
концепции
Эта книга рассказывает о системном nрограммировании, то есть написании системно­

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

-

все это системное программное обеспечение. К данной

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

ми) находится на более высоком уровне и взаимодействует с низкоуровневыми лишь
эпизодически. Некоторые специалисты целыми днями пишут только системное про­

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

-

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

Независимо от того, является такое программирование насущным хлебом конкретно­
го инженера либо просто базисом для создания более высокоуровневых концепций,
системное программирование

-

это сердце всего создаваемого нами софта.

Эта книга посвящена не всему системному программированию, а системному

программированию в

Linux. Linux -

это современная UNIХ-подобная операцион­

ная система, написанная с нуля Линусом Торвальдсом и стихийным сообществом
программистов со всего мира.

Linux

разделяет цели и философию

этом не является лишь разновидностью

UNIX,

UNIX,

но при

а идет своим путем, возвращаясь

в русло

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

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

торически системы

UNIX изначально зарождалось именно как системное. Ис­
UNIX не включали значительного количества высокоуровневых

28

Глава 1. Введение и основополагающие концепции

концепций. Даже при программировании в среде разработки, например в системе
X Window, в полной мере задействовались системные API ядра UNIX. Соответ­
ственно, можно сказать, что эта книга — о программировании для Linux вообще.
Однако учтите, что в книге не рассматриваются среды разработки для Linux, напри­
мер вообще не затрагивается тема make. Основное содержание книги — это API
системного программирования, предоставляемые для использования на современной
машине Linux.
Можно сравнить системное программирование с программированием прило­
жений — и мы сразу заметим как значительное сходство, так и важные различия
этих областей. Важная черта системного программирования заключается в том, что
программист, специализирующийся в этой области, должен обладать глубокими
знаниями оборудования и операционной системы, с которыми он имеет дело. Сис­
темные программы взаимодействуют в первую очередь с ядром и системными
библиотеками, а прикладные опираются и на высокоуровневые библиотеки. Такие
высокоуровневые библиотеки абстрагируют детальные характеристики оборудо­
вания и операционной системы. У подобного абстрагирования есть несколько
целей: переносимость между различными системами, совместимость с разными
версиями этих систем, создание удобного в использовании (либо более мощного,
либо и то и другое) высокоуровневого инструментария. Соотношение, насколько
активно конкретное приложение использует высокоуровневые библиотеки и на­
сколько — систему, зависит от уровня стека, для которого было написано приложе­
ние. Некоторые приложения создаются для взаимодействия исключительно с вы­
сокоуровневыми абстракциями. Однако даже такие абстракции, весьма отдаленные
от самых низких уровней системы, лучше всего получаются у специалиста, имею­
щего навыки системного программирования. Те же проверенные методы и пони­
мание базовой системы обеспечивают более информативное и разумное програм­
мирование для всех уровней стека.

Зачем изучать системное программирование
В течение прошедшего десятилетия в написании приложений наблюдалась тен­
денция к уходу от системного программирования к высокоуровневой разработке.
Это делалось как с помощью веб-инструментов (например, JavaScript), так и по­
средством управляемого кода (Java). Тем не менее такие разработки не свидетель­
ствуют об отмирании системного программирования. Действительно, ведь кому-то
приходится писать и интерпретатор JavaScript, и виртуальную машину Java, кото­
рые создаются именно на уровне системного программирования. Более того, даже
разработчики, которые программируют на Python, Ruby или Scala, только выигра­
ют от знаний в области системного программирования, поскольку будут понимать
всю подноготную машины. Качество кода при этом гарантированно улучшится
независимо от части стека, для которой он будет создаваться.
Несмотря на описанную тенденцию в программировании приложений, большая
часть кода для UNIX и Linux по-прежнему создается на системном уровне. Этот
код написан преимущественно на C и C++ и существует в основном на базе

Системное программирование

29

интерфейсов, предоставляемых библиотекой C и ядром. Это традиционное сис­
темное программирование с применением Apache, bash, cp, Emacs, init, gcc, gdb,
glibc, ls, mv, vim и X. В обозримом будущем эти приложения не сойдут со сцены.
К области системного программирования часто относят и разработку ядра или
как минимум написание драйверов устройств. Однако эта книга, как и большин­
ство работ по системному программированию, никак не касается разработки ядра.
Ее основной фокус — системное программирование для пользовательского про­
странства, то есть уровень, который находится выше ядра. Тем не менее знания
о ядре будут полезным дополнительным багажом при чтении последующего текста.
Написание драйверов устройств — это большая и объемная тема, которая подроб­
но описана в книгах, посвященных конкретно данному вопросу.
Что такое системный интерфейс и как я пишу системные приложения для Linux?
Что именно при этом мне предоставляют ядро и библиотека C? Как мне удается
создавать оптимальный код, какие приемы возможны в Linux? Какие интересные
системные вызовы есть в Linux, но отсутствуют в других UNIX-подобных системах?
Как все это работает? Именно эти вопросы составляют суть данной книги.

Краеугольные камни системного
программирования
В системном программировании для Linux можно выделить три основных крае­
угольных камня: системные вызовы, библиотеку C и компилятор C. О каждом
из этих феноменов следует рассказать отдельно.

Системные вызовы
Системные вызовы — это начало и конец системного программирования. Системные
вызовы (в англоязычной литературе встречается сокращение syscall) — это вызовы
функций, совершаемые из пользовательского пространства. Они направлены из
приложений (например, текстового редактора или вашей любимой игры) к ядру.
Смысл системного вызова — запросить у операционной системы определенную
службу или ресурс. Системные вызовы включают как всем знакомые операции,
например read() и write(), так и довольно экзотические, в частности get_thread_area()
и set_tid_address().
В Linux реализуется гораздо меньше системных вызовов, чем в ядрах большин­
ства других операционных систем. Например, в системах с архитектурой x86-64
таких вызовов насчитывается около 300 — сравните это с Microsoft Windows, где
предположительно задействуются тысячи подобных вызовов. При работе с ядром
Linux каждая машинная архитектура (например, Alpha, x86-64 или PowerPC) может
дополнять этот стандартный набор системных вызовов своими собственными. Сле­
довательно, системные вызовы, доступные в конкретной архитектуре, могут отли­
чаться от доступных в другой. Тем не менее значительное подмножество всех систем­
ных вызовов — более 90 % — реализуется во всех архитектурах. К этим разделяемым
90 % относятся и общие интерфейсы, о которых мы поговорим в данной книге.

30

Глава 1. Введение и основополагающие концепции

Активация системных вызовов. Невозможно напрямую связать приложения
пользовательского пространства с пространством ядра. По причинам, связанным
с обеспечением безопасности и надежности, приложениям пользовательского про­
странства нельзя разрешать непосредственно исполнять код ядра или манипулиро­
вать данными ядра. Вместо этого ядро должно предоставлять механизм, с помощью
которого пользовательские приложения будут «сигнализировать» ядру о требовании
активировать системный вызов. После этого приложение сможет осуществить системное прерывание ядра (trap) в соответствии с этим строго определенным меха­
низмом и выполнить только тот код, который разрешит выполнить ядро. Детали
этого механизма в разных архитектурах немного различаются. Например, в процес­
сорах i386 пользовательское приложение выполняет инструкцию программного
прерывания int со значением 0x80. Эта инструкция осуществляет переключение на
работу с пространством ядра — защищенной областью, — где ядром выполняется
обработчик программного прерывания. Что же такое обработчик прерывания 0x80?
Это не что иное, как обработчик системного вызова!
Приложение сообщает ядру, какой системный вызов требуется выполнить и с ка­
кими параметрами. Это делается посредством аппаратных регистров. Системные
вызовы обозначаются по номерам, начиная с 0. В архитектуре i386, чтобы запросить
системный вызов 5 (обычно это вызов open()), пользовательское приложение за­
писывает 5 в регистр eax, после чего выдает инструкцию int.
Передача параметров обрабатывается схожим образом. Так, в архитектуре i386
регистр применяется для всех возможных параметров — например, регистры ebx, ecx,
edx, esi и edi в таком же порядке содержат первые пять параметров. В редких случаях,
когда системный вызов имеет более пяти параметров, всего один регистр применяется
для указания на буфер в пользовательском пространстве, где хранятся все эти парамет­
ры. Разумеется, у большинства системных вызовов имеется всего пара параметров.
В других архитектурах активация системных вызовов обрабатывается иначе,
хотя принцип остается тем же. Вам, как системному программисту, обычно не нуж­
но знать, как именно ядро обрабатывает системные вызовы. Эта информация уже
интегрирована в стандартные соглашения вызова, соблюдаемые в конкретной ар­
хитектуре, и автоматически обрабатывается компилятором и библиотекой C.

Библиотека C
Библиотека C (libc) — это сердце всех приложений UNIX. Даже если вы програм­
мируете на другом языке, то библиотека C, скорее всего, при этом задействуется.
Она обернута более высокоуровневыми библиотеками и предоставляет основные
службы, а также способствует активации системных вызовов. В современных сис­
темах Linux библиотека C предоставляется в форме GNUlibc, сокращенно glibc
(произносится как «джи-либ-си», реже «глиб-си»).
Библиотека GNU C предоставляет гораздо больше возможностей, чем может
показаться из ее названия. Кроме реализации стандартной библиотеки C, glibc дает
обертки для системных вызовов, поддерживает работу с потоками и основные
функции приложений.

API и ABI

31

Компилятор C
В Linux стандартный компилятор языка C предоставляется в форме коллекции
компиляторов GNU (GNU Compiler Collection, сокращенно gcc). Изначально gcc
представляла собой версию cc (компилятора C) для GNU. Соответственно gcc
расшифровывалась как GNU C Compiler. Однако впоследствии добавилась под­
держка других языков, поэтому сегодня gcc служит общим названием всего семей­
ства компиляторов GNU. При этом gcc — это еще и двоичный файл, используемый
для активации компилятора C. В этой книге, говоря о gcc, я, как правило, имею
в виду программу gcc, если из контекста не следует иное.
Компилятор, используемый в UNIX-подобных системах, в частности в Linux,
имеет огромное значение для системного программирования, поскольку помогает
внедрять стандарт языка C (см. подразд. «Стандарты языка С» разд. «Стандарты»
данной главы), а также системный двоичный интерфейс приложений (см. разд. «API
и ABI» текущей главы), о которых будет рассказано далее.
С++
В этой главе речь пойдет в основном о языке C — лингва франка системного программирования. Однако C++ также играет важную роль.
В настоящее время C++ уступил ведущие позиции в системном программировании своему старшему собрату C. Исторически разработчики Linux всегда отдавали C предпочтение перед C++: основные библиотеки, демоны, утилиты и, разумеется, ядро Linux написаны на C. Влияние C++ как «улучшенного C» в большинстве
«нелинуксовых» систем можно назвать каким угодно, но не универсальным, поэтому
в Linux C++ также занимает подчиненное положение относительно C.
Тем не менее далее в тексте в большинстве случаев вы можете заменять «C» на
«С++». Действительно, C++ — отличная альтернатива C, подходящая для решения
практически любых задач в области системного программирования. C++ может связываться с кодом на C, активизировать системные вызовы Linux, использовать glibc.
При написании на C++ в основу системного программирования закладывается
еще два краеугольных камня — стандартная библиотека C++ и компилятор GNUC++.
Стандартная библиотека C++ реализует системные интерфейсы C++ и использует
стандарт ISOC++ 11. Он обеспечивается библиотекой libstdc++ (иногда используется
название libstdcxx). Компилятор GNUC++ — это стандартный компилятор для кода
на языке C++ в системах Linux. Он предоставляется в двоичном файле g++.

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

32

Глава 1. Введение и основополагающие концепции

На системном уровне существует два отдельных множества определений и опи­
саний, которые влияют на такую переносимость. Одно из этих множеств называет­
ся интерфейсом программирования приложений (Application Programming Interface,
API), а другое — двоичным интерфейсом приложения (Application Binary Interface,
ABI). Обе эти концепции определяют и описывают интерфейсы между различными
компонентами программного обеспечения.

API
API определяет интерфейсы, на которых происходит обмен информацией между
двумя компонентами программного обеспечения на уровне исходного кода. API
обеспечивает абстракцию, предоставляя стандартный набор интерфейсов — как
правило, это функции, — которые один программный компонент (обычно, но не
обязательно это более высокоуровневый компонент из пары) может вызывать из
другого (обычно более низкоуровневого). Например, API может абстрагировать
концепцию отрисовки текста на экране с помощью семейства функций, обеспечи­
вающих все необходимые аспекты для отрисовки текста. API просто определяет
интерфейс; тот компонент программы, который обеспечивает работу API, обычно
называется реализацией этого API.
API часто называют «контрактом». Это неверно как минимум в юридическом
смысле этого слова, поскольку API не имеет ничего общего с двусторонним согла­
шением. Пользователь API (обычно более высокоуровневая программа) распола­
гает нулевым входным сигналом для данного API и реализацией этой сущности.
Пользователь может применять API «как есть» или не использовать его вообще:
возьми или не трогай! Задача API — просто гарантировать, что, если оба компонен­
та ПО воспользуются этим API, они будут совместимы на уровне исходного кода.
Это означает, что пользователь API сможет успешно скомпилироваться с зависи­
мостью от реализации этого API.
Практическим примером API служат интерфейсы, определенные в соответствии
со стандартом C и реализуемые стандартной библиотекой C. Этот API определяет
семейство простейших и критически важных функций, таких как процедуры для
управления памятью и манипуляций со строками.
На протяжении всей книги мы будем опираться на разнообразные API, напри­
мер стандартную библиотеку ввода-вывода, которая будет подробно рассмотрена
в гл. 3. Самые важные API, используемые при системном программировании в Linux,
описаны в разд. «Стандарты» данной главы.

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

Стандарты

33

на двоичном уровне. Это означает, что фрагмент объектного кода будет функ­
ционировать в любой системе с таким же ABI без необходимости перекомпи­
ляции.
ABI помогают решать проблемы, связанные с соглашениями на уровне вызовов,
порядком следования байтов, использованием регистров, активацией системных
вызовов, связыванием, поведением библиотек и форматом двоичных объектов.
Например, соглашения на уровне вызовов определяют, как будут вызываться
функции, как аргументы передаются функциям, какие регистры сохраняются, а ка­
кие — искажаются, как вызывающая сторона получает возвращаемое значение.
Несколько раз предпринимались попытки определить единый ABI для мно­
гих операционных систем, взаимодействующих с конкретной архитектурой
(в частности, для различных UNIX-подобных систем, работающих на i386), но
эти усилия не увенчались какими-либо заметными успехами. Напротив, в опе­
рационных системах, в том числе Linux, сохраняется тенденция к определению
собственных ABI по усмотрению разработчиков. ABI тесно связаны с архитек­
турой; абсолютное большинство ABI оперирует машинно-специфичными кон­
цепциями, в частности Alpha или x86-64. Таким образом, ABI является как
элементом операционной системы (например, Linux), так и элементом архитек­
туры (допустим, x86-64).
Системные программисты должны ориентироваться в ABI, но запоминать их
обычно не требуется. Структура ABI определяется цепочкой инструментов —
компилятором, компоновщиком и т. д. — и никак иначе обычно не проявляется.
Однако знание ABI положительно сказывается на качестве программирования,
а также требуется при написании ассемблерного кода или разработке самой
цепочки инструментов (последняя — классический пример системного програм­
мирования).

Стандарты
Системное программирование для UNIX — старинное искусство. Основы про­
граммирования для UNIX остаются незыблемыми в течение десятилетий. Од­
нако сами системы UNIX развиваются достаточно динамично. Поведение изме­
няется — добавляются новые возможности. Чтобы как-то справиться с хаосом,
целые группы, занятые стандартизацией, кодифицируют системные интерфейсы
в специальных официальных документах. Существует множество таких стан­
дартов, но фактически Linux официально не подчиняется каким-либо из них.
Linux просто стремится соответствовать двум наиболее важным и превалиру­
ющим стандартам — POSIX и Single UNIX Specification (SUS, единая специфи­
кация UNIX).
В POSIX и SUS, в частности, документирован API языка C для интерфейса,
обеспечивающего взаимодействие с UNIX-подобными операционными системами.
Фактически эти стандарты определяют системное программирование или как
минимум его общее подмножество для UNIX-совместимых систем.

34

Глава 1. Введение и основополагающие концепции

История POSIX и SUS
В середине 1980-х годов Институт инженеров по электротехнике и электронике
(Institute of Electrical and Electronics Engineers, IEEE) возглавил начинания по
стандартизации системных интерфейсов в UNIX-подобных операционных систе­
мах. Ричард Столлман (Richard Stallman), основатель движения Free Software,
предложил назвать этот стандарт POSIX (произносится «пазикс», Portable Operating
System Interface — интерфейс переносимых операционных систем UNIX).
Первым результатом этой работы, обнародованным в 1988 году, стал стандарт
IEEE Std 1003.1-1988 (сокращенно POSIX 1988). В 1990 году IEEE пересмотрел
стандарт POSIX, выпустив новую версию IEEE Std 1003.1-1990 (POSIX 1990).
Необязательная поддержка работы в реальном времени и потоков была докумен­
тирована соответственно в стандартах IEEE Std 1003.1b-1993 (POSIX 1993 или
POSIX.1b) и IEEE Std 1003.1c-1995 (POSIX 1995 или POSIX.1c). В 2001 году
необязательные стандарты были объединены с базовым POSIX 1990, образовав
единый стандарт IEEE Std 1003.1-2001 (POSIX 2001). Последняя на этот момент
версия была выпущена в декабре 2008 года и называется IEEE Std 1003.1-2008
(POSIX 2008). Все основные стандарты POSIX сокращаются до аббревиатур вида
POSIX.1, версия от 2008 года является новейшей.
В конце 1980-х — начале 1990-х годов между производителями UNIX-подобных
систем бушевали настоящие «юниксовые войны»: каждый старался закрепить за
своим продуктом статус единственной настоящей UNIX-системы. Несколько
крупных производителей сплотились вокруг The Open Group — промышленного
консорциума, сформировавшегося в результате слияния Open Software Foundation
(OSF) и X/Open. The Open Group стала заниматься сертификацией, публикацией
научных статей и тестированием соответствия. В начале 1990-х годов, когда «юник­
совые войны» были в самом разгаре, The Open Group выпустила Единую специ­
фикацию UNIX (SUS). Популярность SUS быстро росла, во многом благодаря
тому, что она была бесплатной, а стандарт POSIX оставался дорогостоящим. В насто­
ящее время SUS включает в себя новейший стандарт POSIX.
Первая версия SUS была опубликована в 1994 году. Затем последовали пере­
смотренные версии, выпущенные в 1997-м (SUSv2) и 2002 году (SUSv3). Последний
вариант SUS, SUSv4, был опубликован в 2008 году. В SUSv4 пересмотрен стандарт
IEEE Std 1003.1-2008, объединяемый в рамках этой спецификации с несколькими
другими стандартами. В данной книге я буду делать оговорки, когда системные
вызовы и другие интерфейсы стандартизируются по POSIX. Стандартизацию по
SUS я отдельно указывать не буду, так как SUS входит в состав POSIX.

Стандарты языка C
Знаменитая книга Денниса Ричи (Dennis Ritchie) и Брайана Кернигана (Brian
Kernighan) The C Programming Language, впервые опубликованная в 1978 году,
в течение многих лет использовалась как неофициальная спецификация языка C.
Эта версия C была известна в кругах специалистов под названием K&R C. Язык C уже

Стандарты

35

стремительно заменял BASIC и другие языки того времени, превращаясь в лингва
франка микрокомпьютерного программирования, поэтому в 1983 году Американ­
ский национальный институт стандартов (ANSI) сформировал специальный ко­
митет. Этот орган должен был разработать официальную версию C и стандартизи­
ровать самый популярный на тот момент язык программирования. Новая версия
включала в себя разнообразные доработки и усовершенствования, сделанные раз­
личными производителями, а также новый язык C++. Это был долгий и трудоем­
кий процесс, но к 1989 году версия ANSI C была готова. В 1990 году Международ­
ная организация по стандартизации (ISO) ратифицировала стандарт ISO C90,
основанный на ANSI C с небольшимимодификациями.
В 1995 году ISO выпустила обновленную (но редко используемую) версию
языка C, которая называется ISO C95. В 1999 году последовала новая, значительно
пересмотренная версия языка — ISO C99. В ней множество нововведений, в част­
ности внутристрочные функции, новые типы данных, массивы переменной длины,
комментарии в стиле C++ и новые библиотечные функции. Последняя версия
этого стандарта называется ISO C11, в которой следует отметить формализованную
модель памяти. Она обеспечивает переносимость при использовании потоков
в многоплатформенной среде.
Что касается C++, ISO-стандартизация этого языка протекала медленнее.
В 1998 году после долгих лет разработки и выпуска компилятора, не обладавше­
го прямой совместимостью, был ратифицирован первый стандарт C, ISO C98. Он
значительно улучшил совместимость между различными компиляторами, одна­
ко некоторые аспекты этого стандарта ограничивали согласованность и перено­
симость. Стандарт ISOC++03 появился в 2003 году. В нем были исправлены
некоторые ошибки, а также добавлены изменения, облегчившие работу разработ­
чикам компиляторов, но незаметные на пользовательском уровне. Следующий
стандарт ISO, самый актуальный в настоящее время, называется C++11 (ранее
он обозначался как C++0x, поскольку не исключалась более ранняя дата выхода).
В этой версии появилось множество дополнений как на уровне языка, так и в стан­
дартных библиотеках. На самом деле их оказалось настолько большое количест­
во, что многие даже считают язык C++11 совершенно самостоятельным, незави­
симым от более ранних версий C++.

Linux и стандарты
Как было сказано выше, Linux стремится соответствовать стандартам POSIX и SUS.
В Linux предоставляются интерфейсы, документированные в SUSv4 и POSIX 2008,
а также поддерживается работа в реальном времени (POSIX.1b) и работа с пото­
ками (POSIX.1c). Гораздо важнее, что Linux стремится работать в соответствии
с требованиями POSIX и SUS. В принципе, любое несоответствие стандартам
является ошибкой. Считается, что Linux также соответствует POSIX.1 и SUSv3,
но, поскольку никакой официальной сертификации POSIX или SUS не проводилось
(в частности, во всех существующих версиях Linux), нельзя сказать, что Linux
официально соответствует POSIX или SUS.

36

Глава 1. Введение и основополагающие концепции

Что касается языковых стандартов, в Linux все хорошо. Компилятор gcc язы­
ка C соответствует стандарту ISO C99; планируется обеспечить поддержку C11.
Компилятор g++ языка C++ соответствует стандарту ISO C++03, поддержка стан­
дарта C++11 находится в разработке. Кроме того, компиляторы gcc и g++_ реали­
зуют расширения для языков C и C++. Все эти расширения объединяются под
общим названием GNU C и документированы в приложении А.
Linux не может похвастаться большими достижениями в области обеспечения
прямой совместимости1, хотя сегодня и в этой области ситуация значительно улуч­
шилась. Интерфейсы, документированные в соответствии со стандартами, в част­
ности стандартная библиотека C, очевидно, навсегда останутся совместимыми на
уровне исходников. Двоичная совместимость поддерживается как минимум на
уровне основной, крупной версии glibc, а поскольку язык C стандартизирован, gcc
всегда будет компилировать код, написанный на правильном языке C. Правда,
различные специфичные расширения gcc могут устаревать и в конце концов исче­
зать из новых релизов gcc. Важнее всего, что ядро Linux гарантирует стабильность
системных вызовов. Если системный вызов реализован в стабильной версии ядра
Linux, можно быть уверенными, что такой вызов точно сработает.
В различных дистрибутивах Linux многие компоненты операционной системы
определяются в LSB (Linux Standard Base). LSB — это совместный проект несколь­
ких производителей Linux, проводящийся под эгидой Linux Foundation (ранее эта
организация называлась Free Standards Group). LSB дополняет POSIX и SUS,
а также добавляет собственные стандарты. Организация стремится предоставить
двоичный стандарт, позволяющий в неизменном виде выполнять объектный код
в системах, отвечающих этому стандарту. Большинство производителей Linux в той
или иной степени придерживаются LSB.

Стандарты и эта книга
В данной книге я намеренно стараюсь не разглагольствовать о каком-либо стан­
дарте. Слишком часто авторы книг по системному программированию для UNIX
излишне увлекаются сравнениями, как интерфейс работает по одним стандартам
и как по другим, как конкретный системный вызов реализован в той или иной
системе, — словом, льют воду. Эта книга посвящена именно системному програм­
мированию в современных вариантах Linux, в которых используются новейшие
версии ядра Linux (3.9), компилятора gcc (4.8) и библиотеки C (2.17).
Системные интерфейсы можно считать практически неизменными — разработ­
чики ядра Linux проделали огромную работу, чтобы, например, никогда не пришлось
ломать интерфейсы системных вызовов. Эти интерфейсы обеспечивают известный
уровень совместимости на уровне исходного кода и двоичном уровне, поэтому вы­
бранный в данной книге подход позволяет подробно рассмотреть детали системных
1

Возможно, опытные пользователи Linux помнят переход с a.out на ELF, переход с libc5
на glibc, изменения gcc, фрагментацию шаблонов ABI C++ и т. д. К счастью, эти времена
давно позади.

Концепции программирования в Linux

37

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

Концепции программирования в Linux
В этом разделе вы найдете краткий обзор сервисов, предоставляемых в системе
Linux. Все UNIX-подобные системы, включая Linux, предлагают общий набор аб­
стракций и интерфейсов. На самом деле в этой взаимосовместимости и заключает­
ся суть UNIX. Такие абстракции, как файл и процесс, интерфейсы для управления
конвейерами и сокетами и т. д., являются главной составляющей систем UNIX.
Этот обзор предполагает, что вы знакомы с системой Linux. Имеется в виду, что
вы умеете обращаться с командной оболочкой, использовать базовые команды и ком­
пилировать простую программу на C. Это не обзор Linux или системы для програм­
мирования в ней, а рассказ об основах системного программирования Linux.

Файлы и файловая система
Файл — это самая простая и базовая абстракция в Linux. Linux придерживается
философии «все есть файл», пусть и не так строго, как некоторые другие систе­
мы — достаточно вспомнить Plan 91. Следовательно, многочисленные взаимодей­
ствия представляют собой считывание из файлов и запись в них, даже если объект,
с которым вы имеете дело, совсем не похож на «традиционный» файл.
Вообще, чтобы получить доступ к файлу, его сначала нужно открыть. Файлы
можно открывать для чтения, записи или того и другого сразу. На открытый файл
указывает уникальный дескриптор, отображающий метаданные, ассоциированные
с открытым файлом, обратно на сам этот файл. В ядре Linux такой дескриптор
управляется целым числом (в системе типов C целому числу соответствует тип
int). Эта сущность, называемая файловым дескриптором, сокращенно обозначает­
ся fd. Дескрипторы файлов совместно используются в системе и пользовательском
пространстве. Пользовательские программы применяют их непосредственно для
доступа к файлам. Значительная часть системного программирования в Linux
сводится к открытию файлов, манипуляциям с ними, закрытию файлов и исполь­
зованию файловых дескрипторов иными способами.
1

Plan 9 — это операционная система, разработанная в BellLabs и часто характеризующая­
ся как наследница UNIX. В ней воплощено несколько инновационных идей, и она четко
придерживается философии «все есть файл».

38

Глава 1. Введение и основополагающие концепции

Обычные файлы
Сущности, которые большинству из нас известны под названием «файлы», в Linux
именуются обычными файлами. Обычный файл содержит байты данных, органи­
зованные в виде линейного массива, который называется потоком байтов. В Linux
для файла не задается никаких других видов упорядочения или форматирования.
Байты могут иметь любые значения и быть организованы внутри файла любы­
ми способами. На системном уровне Linux не регламентирует для файлов ни­
какой структуры, кроме организации в виде потока байтов. В некоторых опера­
ционных системах, например VMS, используются высокоструктурированные
файлы, в которых применяются так называемые записи. В Linux такие записи
отсутствуют.
Любые байты внутри файла могут использоваться для считывания или запи­
си. Эти операции всегда начинаются с указанного байта, который можно назвать
местоположением в файле. Это местоположение называется файловой позицией
или смещением файла. Файловая позиция — это важнейший элемент метаданных,
который ядро ассоциирует с каждым открытием файла. Когда файл открывается
впервые, его файловая позиция равна нулю. Обычно по мере того, как байты из
файла считываются либо в него записывается информация (байт за байтом),
значение файловой позиции увеличивается. Файловую позицию можно также
вручную устанавливать в определенное значение, причем оно может находиться
даже за пределами (за последним байтом) конкретного файла. Когда файловая
позиция находится за пределами файла, промежуточные байты будут заполнять­
ся нулями. Вполне возможно воспользоваться этим способом и задать файловую
позицию дальше конца файла, однако вы никак не сможете установить эту пози­
цию перед началом файла. Правда, такая практика кажется бессмысленной и дей­
ствительно она почти не применяется. Файловая позиция начинается с нуля; она
не может иметь отрицательное значение. При записи в байт в середине файла
значение, которое ранее находилось по этому смещению, заменяется новым, по­
этому вы не сможете расширить файл, записывая информацию в его середину.
Как правило, запись в файл происходит в его конце. Максимальное значение
файловой позиции ограничено только размером типа C, используемого для хра­
нения файла. В современных системах Linux максимальное значение этого пара­
метра равно 64 бит.
Размер файла измеряется в байтах и называется его длиной. Можно сказать, что
длина — это просто количество байтов в линейном массиве, составляющем файл.
Длину файла можно изменить с помощью операции, которая называется усечением. Файл можно укоротить, уменьшив его размер по сравнению с исходным. В ре­
зультате будут удалены байты, расположенные в конце файла. Термин «усечение»
немного неудачный, поскольку им обозначается и удлинение файла, то есть уве­
личение его размера по сравнению с исходным. В таком случае новые байты (до­
бавляемые в конце файла) заполняются нулями. Файл может быть пуст (иметь
нулевую длину) и, соответственно, не содержать ни одного валидного байта. Мак­
симальная длина файла, как и файловая позиция, ограничена лишь размерами тех
типов C, которые применяются ядром Linux для управления файлами. Однако

Концепции программирования в Linux

39

в конкретных файловых системах могут действовать собственные ограничения,
из-за которых потолок длины файла существенно снижается.
Отдельно взятый файл можно одновременно открыть несколько раз как в ином,
так и в том же самом процессе. Каждому открытому экземпляру файла присваива­
ется уникальный дескриптор. С другой стороны, процессы могут совместно ис­
пользовать свои файловые дескрипторы, один дескриптор может применяться в не­
скольких процессах. Ядро не накладывает никаких ограничений на параллельный
доступ к файлу. Множественные процессы вполне могут одновременно считывать
информацию из файла и записывать туда новые данные. Результаты такой парал­
лельной работы зависят от упорядочения отдельных операций и, в принципе,
непредсказуемы. Программы пользовательского пространства обычно должны
взаимно координироваться, чтобы обеспечить правильную синхронизацию парал­
лельных обращений к файлам.
Хотя доступ к файлам обычно осуществляется по их именам, непосредственная
связь файла с его названием отсутствует. В действительности ссылка на файл вы­
полняется по индексному дескриптору1. Этому дескриптору присваивается цело­
численное значение, уникальное для файловой системы (но не обязательно уни­
кальное во всей системе в целом). Данное значение называется номером индексного
дескриптора. В индексном дескрипторе сохраняются метаданные, ассоциированные
с файлом, например отметка о времени его последнего изменения, владелец файла,
тип, длина и местоположение данных файла, но имя файла там не сохраняется!
Индексный дескриптор одновременно является и физическим объектом, располо­
женным на диске в UNIX-подобной файловой системе, и концептуальной сущностью,
представленной как структура данных в ядре Linux.

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

См.: http://ru.wikipedia.org/wiki/Inode. — Примеч. пер.

40

Глава 1. Введение и основополагающие концепции

индексного дескриптора. По этому номеру находится сам индексный дескриптор.
Индексный дескриптор содержит метаданные, ассоциированные с файлом, в част­
ности информацию о том, в каком именно фрагменте диска записаны данные
этого файла.
Сначала на диске присутствует лишь один корневой каталог. К нему обычно
ведет путь /. Однако, как известно, в любой системе, как правило, множество ката­
логов. Как ядро узнает, в каком именно нужно искать файл с заданным именем?
Выше мы говорили о том, что каталоги во многом похожи на обычные файлы.
Действительно, с ними даже ассоциированы свои индексные дескрипторы, поэто­
му ссылки внутри каталогов могут указывать на индексные дескрипторы, находя­
щиеся в других каталогах. Это означает, что одни каталоги можно вкладывать в дру­
гие, образуя иерархические структуры, что, в свою очередь, позволяет использовать
полные пути к элементам, знакомые каждому пользователю UNIX, например
/home/blackbeard/concorde.png.
Когда мы запрашиваем у ядра открытие подобного пути к файлу, оно обходит
все записи каталогов, указанные в пути к элементу. Так удается найти индексный
дескриптор следующей записи. В предыдущем примере ядро начинает работу с /,
получает индексный дескриптор home, идет туда, получает индексный дескриптор
blackbeard, идет туда и, наконец, получает индексный дескриптор concorde.png. Эта
операция называется разрешением каталога или разрешением пути к элементу.
Кроме того, в ядре Linux используется кэш, называемый кэшем каталогов. В кэше
каталогов сохраняются результаты разрешения каталогов, впоследствии обеспе­
чивающие более быстрый поиск с учетом временной локальности1.
Если имя пути начинается с корневого каталога, говорят, что путь полностью
уточнен. Его называют абсолютным путем к элементу. Некоторые имена путей
уточнены не полностью, а указываются относительно какого-то другого каталога
(например, todo/plunder). Такие пути называются относительными. Если ядру
предоставляется относительный путь, то оно начинает разрешение пути с текущего рабочего каталога. Отсюда ядро ищет путь к каталогу todo. В каталоге todo ядро
получает индексный дескриптор plunder. В результате комбинации относительно­
го пути к элементу и пути к текущему рабочему каталогу получается полностью
уточненный путь.
Хотя каталоги и воспринимаются как обычные файлы, ядро не позволяет их
открывать и производить с ними те же манипуляции, что и с обычными файлами.
Для работы с каталогами используется специальный набор системных вызовов.
Эти системные вызовы предназначаются для добавления и удаления ссылок —
в принципе, на этом перечень разумных операций с каталогами заканчивается. Если
бы можно было манипулировать каталогами прямо из пользовательского простран­
ства, без посредничества ядра, то единственной простой ошибки хватило бы для
повреждения всей файловой системы.
1

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

Концепции программирования в Linux

41

Жесткие ссылки
С учетом всего вышесказанного ничто вроде бы не препятствует разрешению мно­
жества имен в один и тот же индексный дескриптор. Действительно, это допуска­
ется. Когда множественные ссылки отображают различные имена на один и тот же
индексный дескриптор, эти ссылки называются жесткими.
Благодаря жестким ссылкам в файловых системах обеспечивается создание
сложных структур, где множественные имена путей могут указывать на одни и те
же данные. Эти жесткие ссылки могут находиться в одном каталоге, а также в двух
и более различных каталогах. В любом случае ядро просто разрешает имя пути в вер­
ный индексный дескриптор. Например, можно поставить жесткую ссылку на кон­
кретный индексный дескриптор, ссылающийся на определенный фрагмент данных
из двух мест — /home/bluebeard/treasure.txt и /home/blackbeard/to_steal.txt.
При удалении файла он отсоединяется от структуры каталогов. Для этого нуж­
но просто удалить из каталога пару, в которой содержится имя файла и его индекс­
ный дескриптор. Однако, поскольку в Linux поддерживаются жесткие ссылки,
файловая система не может просто уничтожать индексный дескриптор и ассоци­
ированные с ним данные при каждой операции удаления. Что, если на этот файл
были проставлены и другие жесткие ссылки из файловой системы? Чтобы гаран­
тировать, что файл не будет уничтожен, пока не исчезнут все указывающие на него
жесткие ссылки, в каждом индексном дескрипторе содержится счетчик ссылок,
отслеживающий количество ссылок в файловой системе, указывающих на этот
дескриптор. Когда путь к элементу отсоединяется от файловой системы, значение
этого счетчика уменьшается на 1. Лишь если значение счетчика ссылок достигает
нуля, и индексный дескриптор, и ассоциированные с ним данные окончательно
удаляются из файловой системы.

Символьные ссылки
Жесткие ссылки не могут связывать файловые системы, поскольку номер индекс­
ного дескриптора не имеет смысла вне его собственной файловой системы. Чтобы
ссылки могли соединять информацию из различных файловых систем, становясь
при этом и более простыми, и менее прозрачными, в системах UNIX применяются
так называемые символьные ссылки.
Символьные ссылки похожи на обычные файлы. Такая ссылка имеет свой ин­
дексный дескриптор и ассоциированный с ним фрагмент данных, содержащий
полное имя пути к связанному файлу. Таким образом, символьные ссылки могут
указывать куда угодно, в том числе на файлы и каталоги, расположенные в иных
файловых системах, и даже на несуществующие файлы и каталоги. Символьная
ссылка, указывающая на несуществующий файл, называется сломанной.
C использованием символьных ссылок связано больше издержек, чем при ра­
боте с жесткими ссылками, так как символьная ссылка, в сущности, требует разре­
шения двух файлов: самой символьной ссылки и связанного с ней файла. При ис­
пользовании жестких ссылок такие дополнительные затраты отсутствуют — нет
разницы между обращениями к файлам, обладающим одной связью в файловой

42

Глава 1. Введение и основополагающие концепции

системе либо несколькими связями. Издержки при работе с символьными ссыл­
ками минимальны, но тем не менее они воспринимаются отрицательно.
Кроме того, символьные ссылки менее прозрачны, чем жесткие. Использование
жестких ссылок — совершенно очевидный процесс. Более того, не так просто най­
ти файл, на который проставлено несколько жестких ссылок! Для манипуляций
же с символьными ссылками требуются специальные системные вызовы. Эта не­
прозрачность зачастую воспринимается как положительный момент, так как в сим­
вольной ссылке ее структура выражается открытым текстом. Символьные ссылки
используются именно как инструменты быстрого доступа (ярлыки), а не как
внутрисистемные ссылки.

Специальные файлы
Специальные файлы — это объекты ядра, представленные в виде файлов. С годами
в системах UNIX накопилось множество типов поддерживаемых специальных
файлов. В Linux поддерживается четыре типа таких файлов: файлы блочных
устройств, файлы символьных устройств, именованные каналы1 и доменные соке­
ты UNIX. Специальные файлы обеспечивают возможности встраивания опреде­
ленных абстракций в файловую систему и, таким образом, поддерживают парадиг­
му «все есть файл». Для создания специального файла в Linux предоставляется
специальный системный вызов.
Доступ к устройствам в системах UNIX осуществляется через файлы устройств,
которые выглядят и действуют как обычные файлы, расположенные в файловой
системе. Файлы устройств можно открывать, считывать из них информацию и за­
писывать ее в них. Из пользовательского пространства можно получать доступ к фай­
лам устройств и манипулировать устройствами в системе (как физическими, так
и виртуальными). Как правило, все устройства в UNIX подразделяются на две
группы — символьные устройства и блочные устройства. Каждому типу устрой­
ства соответствует свой специальный файл устройства.
Доступ к символьному устройству осуществляется как к линейной последова­
тельности байтов. Драйвер устройства ставит байты в очередь, один за другим, а про­
грамма из пользовательского пространства считывает байты в порядке, в котором
они были помещены в очередь. Типичным примером символьного устройства яв­
ляется клавиатура. Если пользователь наберет на клавиатуре последовательность
peg, то приложению потребуется считать из файла-устройства клавиатуры снача­
ла p, потом e и, наконец, g — именно в таком порядке. Когда больше не остается
символов, которые необходимо прочитать, устройство возвращает «конец файла»
(EOF). Если какой-то символ будет пропущен или символы будут прочтены в не­
правильном порядке, то операция получится фактически бессмысленной. Доступ
к символьным устройствам происходит через файлы символьных устройств.
1

Именованный канал называется в оригинале named pipe, более точный перевод — имено­
ванный конвейер. Тем не менее мы остановимся на варианте «именованный канал»
(http://ru.wikipedia.org/wiki/Именованный_канал), как на более употребительном в рус­
ском языке, а термин pipe будем далее переводить как «конвейер». — Примеч. пер.

Концепции программирования в Linux

43

Напротив, доступ к блочному устройству происходит как к массиву байтов.
Драйвер устройства отображает массив байтов на устройство с возможностью
позиционирования, и пользовательское пространство может в произвольном по­
рядке обращаться к любым валидным байтам массива, то есть можно сначала
прочитать байт 12, потом байт 7, потом опять байт 12. Блочные устройства — это
обычно устройства для хранения информации. Жесткие диски, дисководы гибких
дисков, CD-ROM, флэш-накопители — все это примеры блочных устройств. До­
ступ к ним осуществляется через файлы блочных устройств.
Именованные каналы (часто обозначаемые аббревиатурой FIFO — «первым при­
шел, первым обслужен») — это механизм межпроцессного взаимодействия (IPC),
предоставляющего канал связи для дескриптора файла. Доступ к именованному
каналу выполняется через специальный файл. Обычные конвейеры применяются
именно для того, чтобы «перекачивать» вывод одной программы во ввод другой; они
создаются в памяти посредством системного вызова и не существуют в какой-либо
файловой системе. Именованные каналы действуют как и обычные, но обращение
к ним происходит через файл, называемый специальным файлом FIFO. Несвязанные
процессы могут обращаться к этому файлу и обмениваться информацией.
Последний тип специальных файлов — это сокеты. Сокеты обеспечивают усо­
вершенствованную разновидность межпроцессного взаимодействия между двумя
несвязанными процессами — не только на одной машине, но и даже на двух разных.
На самом деле сокеты являются основополагающей концепцией всего программи­
рования для сетей и Интернета. Существует множество разновидностей сокетов,
в том числе доменные сокеты UNIX. Последние используются для взаимодействия
на локальной машине. В то время как сокеты, обменивающиеся информацией по
Интернету, могут использовать пару из хост-имени и порта для идентификации
«цели» взаимодействия, доменные сокеты используют для этого специальный файл,
расположенный в файловой системе. Часто его называют просто сокет-файлом.

Файловые системы и пространства имен
Linux, как и все системы UNIX, предоставляет глобальное и единое пространство
имен для файлов и каталогов. В некоторых операционных системах отдельные
физические и логические диски разделяются на самостоятельные пространства
имен. Например, для доступа к файлу на дискете может использоваться путь A:\
plank.jpg, а пути ко всем файлам на жестком диске будут начинаться с C:\. В UNIX
тот же файл с дискеты может быть доступен через /media/floppy/plank.jpg или даже
через /home/captain/stuff/plank.jpg, в одном ряду с файлами с других носителей.
В этом и выражается единство пространства имен в UNIX.
Файловая система — это набор файлов и каталогов формальной и валидной
иерархии. Файловые системы можно по отдельности добавлять к глобальному
пространству имен и удалять их из этого глобального пространства файлов
и каталогов. Данные операции называются монтированием и размонтированием. Каждая файловая система может быть индивидуально монтирована к кон­
кретной точке пространства имен. Она обычно называется точкой монтирования. В дальнейшем из точки монтирования открывается доступ к корневому

44

Глава 1. Введение и основополагающие концепции

каталогу файловой системы. Например, CD может быть монтирован в точке /me­
dia/cdrom, в результате чего корневой каталог файловой системы компакт-дис­
ка также будет доступен через /media/cdrom. Файловая система, которая была
монтирована первой, находится в корне пространства имен, / , и называется
корневой файловой системой. Во всех системах Linux всегда имеется корневая
файловая система. Монтирование других файловых систем в тех или иных
точках в Linux не является обязательным.
Файловые системы обычно существуют физически (то есть сохраняются на
диске), хотя в Linux также поддерживаются виртуальные файловые системы, су­
ществующие лишь в памяти, и сетевые файловые системы, существующие одно­
временно на нескольких машинах, работающих в сети. Физические файловые
системы находятся на блочных устройствах хранения данных, в частности на
компакт-дисках, дискетах, картах флэш-памяти, жестких дисках. Некоторые из
этих устройств являются сегментируемыми — это означает, что их дисковое про­
странство можно разбить на несколько файловых систем, каждой из которых
можно управлять отдельно. В Linux поддерживаются самые разные файловые
системы, решительно любые, которые могут встретиться среднему пользователю
на практике. В частности, Linux поддерживает файловые системы, специфичные
для определенных носителей (например, ISO9660), сетевые файловые системы
(NFS), нативные файловые системы (ext4), файловые системы из других разно­
видностей UNIX (XFS), а также файловые системы, не относящиеся к семейству
UNIX (FAT).
Наименьшим адресуемым элементом блочного устройства является сектор.
Сектор — это физический атрибут устройства. Объем секторов может быть равен
различным степеням двойки, довольно распространены секторы размером 512 байт.
Блочное устройство не может передавать элемент данных размером меньше, чем
сектор этого устройства, а также не может получать доступ к такому мелкому
фрагменту данных. При операциях ввода-вывода должны задействоваться один
или несколько секторов.
Аналогично, наименьшим логически адресуемым элементом файловой системы
является блок. Блок — это абстракция, применяемая в файловой системе, а не на
физическом носителе, на котором эта система находится. Обычно размер блока
равен степени двойки от размера сектора. В Linux блоки, как правило, крупнее
сектора, но они должны быть меньше размера страницы (под страницей в модели
памяти Linux понимается наименьший элемент, адресуемый блоком управления
памятью — аппаратным компонентом)1. Размеры большинства блоков составляют
512 байт, 1 Кбайт и 4 Кбайт.
Исторически в системах UNIX было только одно разделяемое пространство
имен, видимое для всех пользователей и всех процессов в системе. В Linux исполь­
зуется инновационный подход и поддерживаются пространства имен отдельных
процессов. В таком случае каждый процесс может иметь уникальное представление
1

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

Концепции программирования в Linux

45

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

Процессы
Если файлы являются самой фундаментальной абстракцией системы UNIX, то
следующая по важности — процесс. Процессы — это объектный код, находящийся
в процессе исполнения: активные, работающие программы. Однако процессы — это
не просто объектный код, так как они состоят из данных, ресурсов, состояния и вир­
туализованного процессора.
Процесс начинает свой жизненный цикл в качестве исполняемого объектного
кода. Это код в формате, пригодном для исполнения на машине и понятный ядру.
Наиболее распространенный подобный формат в Linux называется форматом ис­
полняемых и компонуемых файлов (ELF). Исполняемый формат содержит мета­
данные и множество разделов с кодом и данными. Разделы — это линейные фраг­
менты объектного кода, именно в такой линейной форме загружаемые в память.
Все байты одного раздела обрабатываются одинаково, имеют одни и те же права
доступа и, как правило, используются в одних и тех же целях.
К самым важным и распространенным разделам относятся текстовый раздел,
раздел данных и раздел bss. В текстовом разделе содержатся исполняемый код
и данные только для чтения, в частности константные переменные. Обычно этот
раздел помечается как доступный только для чтения и исполняемый. В разделе
данных хранятся инициализированные данные (например, переменные C с опре­
деленными значениями). Обычно этот раздел помечается как доступный для чтения
и записи. Раздел bss содержит неинициализированные глобальные данные. Стан­
дарт C требует, чтобы все глобальные переменные C по умолчанию инициализи­
ровались нулями, поэтому нет необходимости хранить нули в объектном коде на
диске. Вместо этого объектный код может просто перечислять неинициализиро­
ванные переменные в разделе bss, а при загрузке раздела в память ядро отобразит
на него нулевую страницу (страницу, содержащую только нули). Раздел bss заду­
мывался исключительно в качестве оптимизации. Название bss, в сущности,
является пережитком, оно означает block started by symbol (блок, начинающийся
с символа). Другими примерами распространенных разделов исполняемых файлов
в формате ELF являются абсолютный раздел (содержащий неперемещаемые сим­
волы) и неопределенный раздел, также называемый общей корзиной (catchall).
Кроме того, процесс ассоциирован с различными системными ресурсами, кото­
рые выделяются и управляются ядром. Как правило, процессы запрашивают ре­
сурсы и манипулируют ими только посредством системных вызовов. К ресурсам
относятся таймеры, ожидающие сигналы, открытые файлы, сетевые соединения,
1

Этот подход был впервые реализован в операционной системе Plan 9 производства
BellLabs.

46

Глава 1. Введение и основополагающие концепции

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

Потоки
Каждый процесс состоит из одного или нескольких потоков выполнения, обычно
называемых просто потоками. Поток — это единица активности в процессе. Можно
также сказать, что поток — это абстракция, отвечающая за выполнение кода и под­
держку процесса в рабочем состоянии.
Большинство процессов состоят только из одного потока и именуются однопоточными. Если процесс содержит несколько потоков, его принято называть многопоточным. Традиционно программы UNIX являются однопоточными, это свя­
зано с присущей UNIX простотой, быстрым созданием процессов и надежными
механизмами межпроцессного взаимодействия. По всем этим причинам многопо­
точность не имеет в UNIX существенного значения.
Поток состоит из стека (в котором хранятся локальные переменные этого про­
цесса, точно как в стеке процесса, используемом в однопоточных системах), состоя­
ния процесса и актуального местоположения в объектном коде. Информация об этом
местоположении обычно хранится в указателе команд процесса. Большинство
остальных элементов процесса разделяются между всеми его потоками, особенно это
касается адресного пространства. Таким образом, потоки совместно используют
абстракцию виртуальной памяти, поддерживая абстракцию виртуализованного
процессора.
На внутрисистемном уровне в ядре Linux реализуется уникальная разновидность
потоков: фактически они представляют собой обычные процессы, которые по мере
необходимости разделяют определенные ресурсы. В пользовательском простран­
стве Linux реализует потоки в соответствии со стандартом POSIX 1003.1c (также

Концепции программирования в Linux

47

называемым Pthreads). Самый современный вариант реализации потоков в Linux
именуется Native POSIX Threading Library (NPTL). Эта библиотека входит в состав
glibc. Подробнее мы поговорим о потоках в гл. 7.

Иерархия процессов
Для идентификации каждого процесса применяется уникальное положительное
целое число, называемое идентификатором процесса, или ID процесса (pid). Таким
образом, первый процесс имеет идентификатор 1, а каждый последующий процесс
получает новый, уникальный pid.
В Linux процессы образуют строгую иерархию, называемую деревом процессов.
Корень дерева находится в первом процессе, называемом процессом инициализации
и обычно принадлежащем программе init. Новые процессы создаются с помощью
системного вызова fork(). В результате этого вызова создается дубликат вызыва­
ющего процесса. Исходный процесс называется предком, а порожденный — потомком. У каждого процесса, кроме самого первого, есть свой предок. Если родительский
процесс завершается раньше дочернего (потомка), то ядро переназначает предка
для потомка, делая его потомком процесса инициализации.
Когда процесс завершается, он еще какое-то время остается в системе. Ядро
сохраняет фрагменты процесса в памяти, обеспечивая процессу-предку доступ
к информации о процессе-потомке, актуальной на момент завершения потомка.
Такое запрашивание называется обслуживанием завершенного процесса. Когда
родительский процесс обслужит дочерний, последний полностью удаляется. Про­
цесс, который уже завершился, но еще не был обслужен, называется зомби. Ини­
циализирующий процесс по порядку обслуживает все свои дочерние процессы,
гарантируя, что процессы с переназначенными родителями не останутся в состоя­
нии зомби на неопределенный срок.

Пользователи и группы
Авторизация в Linux обеспечивается с помощью системы пользователей и групп.
Каждому пользователю присваивается уникальное положительное целое число,
называемое пользовательским ID (uid). Каждый процесс, в свою очередь, ассоци­
ируется ровно с одним uid, обозначающим пользователя, который запустил процесс.
Этот идентификатор называется реальным uid процесса. Внутри ядра Linux uid
является единственной концепцией, представляющей пользователя. Однако поль­
зователи называют себя и обращаются к другим пользователям по именам пользователей (username), а не по числовым значениям. Пользовательские имена и соот­
ветствующие им uid сохраняются в каталоге /etc/passwd, а библиотечные процедуры
сопоставляют предоставленные пользователями имена и соответствующие uid.
При входе в систему пользователь вводит свое имя и пароль в программу входа
(login). Если эта программа получает верную пару, состоящую из логина и пароля,
то login порождает пользовательскую оболочку входа, также указываемую в /etc/
passwd, и делает uid оболочки равным uid вошедшего пользователя. Процессы-по­
томки наследуют uid своих предков.

48

Глава 1. Введение и основополагающие концепции

Uid 0 соответствует особому пользователю, который называется root. Этот поль­
зователь обладает особыми привилегиями и может делать в системе практически
что угодно. Например, только root-пользователь имеет право изменить uid процес­
са. Следовательно, программа входа (login) работает как root.
Кроме реального uid, каждый процесс также обладает действительным uid,
сохраненным uid и uid файловой системы. В то время как реальный uid процесса
всегда совпадает с uid пользователя, запустившего процесс, действительный uid
может изменяться по определенным правилам, чтобы процесс мог выполняться
с правами, соответствующими различным пользователям. В сохраненном uid за­
писывается исходный действительный uid; его значение используется при опреде­
лении, на какие значения действительного uid может переключаться пользователь.
Uid файловой системы, который, как правило, равен действительному uid, исполь­
зуется для верификации доступа к файловой системе.
Каждый пользователь может принадлежать к одной или нескольким группам,
в частности к первичной группе, она же группа входа в систему, указанной в /etc/
passwd, а также к нескольким дополнительным группам, которые перечисляются
в /etc/group. Таким образом, каждый процесс ассоциируется с соответствующим
групповым ID (gid) и имеет действительный gid, сохраненный gid и gid файловой
системы. Обычно процессы ассоциированы с пользовательской группой входа, а не
с какими-либо дополнительными группами.
Определенные проверки безопасности позволяют процессам выполнять те или
иные операции лишь при условии, что процесс соответствует заданным критериям.
Исторически это решение в UNIX было жестко детерминированным: процессы
с uid 0 имели доступ, а остальные — нет. Сравнительно недавно в Linux эта система
была заменена более универсальной, в которой используется концепция возможностей (capabilities). Вместо обычной двоичной проверки ядро, с учетом возможностей,
может предоставлять доступ на базе гораздо более филигранных настроек.

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

49

Концепции программирования в Linux

что пользователь может открыть каталог и увидеть список его содержимого. Право
записи позволяет добавлять в каталог новые ссылки, а право исполнения разрешает
открыть каталог и ввести его название в имя пути. В табл. 1.1 перечислены все девять
бит доступа, их восьмеричные значения (распространенный способ представления
девяти бит), их текстовые значения (отображаемые командой ls) и их права.
Таблица 1.1. Биты доступа и их значения
Бит
8
7
6
5
4
3
2
1
0

Восьмеричное значение
400
200
100
040
020
010
004
002
001

Текстовое значение
r--------w--------x--------r--------w--------x--------r--------w--------x

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

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

Сигналы
Сигналы — это механизм, обеспечивающий односторонние асинхронные уведом­
ления. Сигнал может быть отправлен от ядра к процессу, от процесса к другому
процессу либо от процесса к самому себе. Обычно сигнал сообщает процессу, что
произошло какое-либо событие, например возникла ошибка сегментации или
пользователь нажал Ctrl+C.
Ядро Linux реализует около 30 разновидностей сигналов (точное количество
зависит от конкретной архитектуры). Каждый сигнал представлен числовой кон­
стантой и текстовым названием. Например, сигнал SIGHUP используется, чтобы сооб­
щить о зависании терминала. В архитектуре x86-64 этот сигнал имеет значение 1.
Сигналы прерывают исполнение работающего процесса. В результате процесс
откладывает любую текущую задачу и немедленно выполняет заранее определен­
ное действие. Все сигналы, за исключением SIGKILL (всегда завершает процесс)
и SIGSTOP (всегда останавливает процесс), оставляют процессам возможность вы­
бора того, что должно произойти после получения конкретного сигнала. Так, про­
цесс может совершить действие, заданное по умолчанию (в частности, завершение
процесса, завершение процесса с созданием дампа, остановка процесса или отсут­
ствие действия), — в зависимости от полученного сигнала. Кроме того, процессы
могут явно выбирать, будут они обрабатывать сигнал или проигнорируют его.

50

Глава 1. Введение и основополагающие концепции

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

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

Заголовки
Системное программирование в Linux всецело зависит от нескольких заголовков.
И само ядро, и glibc предоставляют заголовки, применяемые при системном про­
граммировании. К ним относятся стандартный джентльменский набор C (например,
) и обычные заголовки UNIX (к примеру, ).

Обработка ошибок
Само собой разумеется, что проверка наличия ошибок и их обработка — задача пер­
востепенной важности. В системном программировании ошибка характеризуется
возвращаемым значением функции и описывается с помощью специальной пере­
менной errno. glibc явно предоставляет поддержку errno как для библиотечных, так
и для системных вызовов. Абсолютное большинство интерфейсов, рассмотренных
в данной книге, используют для сообщения об ошибках именно этот механизм.
Функции уведомляют вызывающую сторону об ошибках посредством специаль­
ного возвращаемого значения, которое обычно равно –1 (точное значение, использу­
емое в данном случае, зависит от конкретной функции). Значение ошибки предупре­
ждает вызывающую сторону, что возникла ошибка, но никак не объясняет, почему
она произошла. Переменная errno используется для выяснения причины ошибки.
Эта переменная объявляется в следующим образом:
extern int errno;

Концепции программирования в Linux

51

Ее значение является валидным лишь непосредственно после того, как функция,
задающая errno, указывает на ошибку (обычно это делается путем возвращения -1).
Дело в том, что после успешного выполнения функции значение этой переменной
вполне может быть изменено.
Переменная errno доступна для чтения или записи напрямую; это модифициру­
емое именуемое выражение. Значение errno соответствует текстовому описанию
конкретной ошибки. Препроцессор #define также ассоциирует переменную errno
с числовым значением. Например, препроцессор определяет EACCES равным 1
и озна­чает «доступ запрещен». В табл. 1.2 перечислены стандартные определения
и соответствующие им описания ошибок.
Таблица 1.2. Ошибки и их описание
Обозначение препроцессора
E2BIG
EACCES
EAGAIN
EBADF
EBUSY
ECHILD
EDOM
EEXIST
EFAULT
EFBIG
EINTR
EINVAL
EIO
EISDIR
EMFILE
EMLINK
ENFILE
ENODEV
ENOENT
ENOEXEC
ENOMEM
ENOSPC
ENOTDIR
ENOTTY
ENXIO
EPERM
EPIPE
ERANGE
EROFS
ESPIPE
ESRCH
ETXTBSY
EXDEV

Описание
Список аргументов слишком велик
Доступ запрещен
Повторить попытку (ресурс временно недоступен)
Недопустимый номер файла
Заняты ресурс или устройство
Процессы-потомки отсутствуют
Математический аргумент вне области функции
Файл уже существует
Недопустимый адрес
Файл слишком велик
Системный вызов был прерван
Недействительный аргумент
Ошибка ввода-вывода
Это каталог
Слишком много файлов открыто
Слишком много ссылок
Переполнение таблицы файлов
Такое устройство отсутствует
Такой файл или каталог отсутствует
Ошибка формата исполняемого файла
Недостаточно памяти
Израсходовано все пространство на устройстве
Это не каталог
Недопустимая операция управления вводом-выводом
Такое устройство или адрес не существует
Операция недопустима
Поврежденный конвейер
Результат слишком велик
Файловая система доступна только для чтения
Недействительная операция позиционирования
Такой процесс отсутствует
Текстовый файл занят
Неверная ссылка

52

Глава 1. Введение и основополагающие концепции

Библиотека C предоставляет набор функций, позволяющих представлять кон­
кретное значение errno в виде соответствующего ему текстового представления.
Эта возможность необходима только для отчетности об ошибках и т. п. Проверка
наличия ошибок и их обработка может выполняться на основе одних лишь опре­
делений препроцессора, а также напрямую через errno.
Первая подобная функция такова:
#include
void perror (const char *str);

Эта функция печатает на устройстве stderr (standard error — «стандартная
ошибка») строковое представление текущей ошибки, взятое из errno, добавляя
в качестве префикса строку, на которую указывает параметр str. Далее следует
двоеточие. Для большей информативности имя отказавшей функции следует
включать в строку. Например:
if (close (fd) == –1)
perror ("close");

В библиотеке C также предоставляются функции strerror() и strerror_r(),
прототипированные как:
#include
char * strerror (int errnum);

и
#include
int strerror_r (int errnum, char *buf, size_t len);

Первая функция возвращает указатель на строку, описывающую ошибку, вы­
данную errnum. Эта строка не может быть изменена приложением, однако это мож­
но сделать с помощью последующих вызовов perror() и strerror(). Соответственно,
такая функция не является потокобезопасной.
Функция strerror_r(), напротив, является потокобезопасной. Она заполняет
буфер, имеющий длину len, на который указывает buf. При успешном вызове
strerror_r() возвращается 0, при сбое — –1. Забавно, но при ошибке она выдает
errno.
Для некоторых функций к допустимым возвращаемым значениям относится
вся область возвращаемого типа. В таких случаях перед вызовом функции errno
нужно присвоить значение 0, а по завершении проверить ее новое значение (такие
функции могут возвратить ненулевое значение errno, только когда действительно
возникла ошибка). Например:
errno = 0;
arg = strtoul (buf, NULL, 0);
if (errno)
perror ("strtoul");

Добро пожаловать в системное программирование

53

При проверке errno мы зачастую забываем, что это значение может быть изме­
нено любым библиотечным или системным вызовом. Например, в следующем коде
есть ошибка:
if (fsync (fd) == –1) {
fprintf (stderr, "fsyncfailed!\n");
if (errno == EIO)
fprintf (stderr, "I/O error on %d!\n", fd);
}

Если необходимо сохранить значение errno между вызовами нескольких функ­
ций, делаем это:
if (fsync (fd) == –1) {
const int err = errno;
fprintf (stderr, "fsync failed: %s\n", strerror (errno));
if (err == EIO) {
/* если ошибка связана с вводом-выводом — уходим */
fprintf (stderr, "I/O error on %d!\n", fd);
exit (EXIT_FAILURE);
}
}

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

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

2

ФаЙЛОВЬIЙ BBOA-ВЬIВОА

в этой и трех следующих главах мы поговорим о файлах. В UNIХ-подобных сис­
темах много сущностей представлено именно в виде файлов, поэтому данные

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

UNIX.

В этой главе мы обсудим основы

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

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

4

эта тема

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

8

данные темы

получают логическое завершение: в ней рассматриваются манипуляции, выпол­

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

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

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

jd).

Каждая запись в списке содержит информацию об

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

уникальными

cookie.

При открытии файла возвращается его дескриптор, а при

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

файла принимается как первичный аргумент.

В языке С дескрипторы файлов относятся к типу l nt. Факт, что для них не вы­
делен специальный тип, зачастую считается странным, но исторически именно

такая ситуация сложилась в

UNIX.

ДЛЯ каждого процесса

Linux

задается макси­

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

мального. По умолчанию максимальное значение равно 1024, но при необходимости
его можно повысить до 1 048 576. Файловые дескрипторы не могут иметь отрица­

тельных значений, поэтому значение

-1 часто применяется для индикации ошибки,

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

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

55

Каждый процесс традиционно имеет не менее трех открытых файловых дес­
крипторов: 0, 1 и 2, если, конечно, процесс явно не закрывает один из них. Файловый
дескриптор 0 соответствует стандартному вводу (stdin), дескриптор 1 — стандартному выводу (stdout), дескриптор 2 — стандартной ошибке (stderr). Библиоте­
ка С не ссылается непосредственно на эти целые числа, а предоставляет препроцес­
сорные определения STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO для каждого из
вышеописанных вариантов соответственно. Как правило, stdin подключен к тер­
минальному устройству ввода (обычно это пользовательская клавиатура), а stdout
и stderr — к дисплею терминала. Пользователи могут переназначать эти стандарт­
ные файловые дескрипторы и даже направлять по конвейеру вывод одной програм­
мы во ввод другой. Именно так оболочка реализует переназначения и конвейеры.
Дескрипторы могут ссылаться не только на обычные файлы, но и на файлы
устройств и конвейеры, каталоги и фьютексы, FIFO и сокеты. Это соответствует
парадигме «все есть файл». Практически любая информация, которую можно читать
или записывать, доступна по файловому дескриптору.
По умолчанию процесс-потомок получает копию таблицы файлов своего про­
цесса-предка. Список открытых файлов и режимы доступа к ним, актуальные
файловые позиции и другие метаданные не меняются. Однако изменение, связан­
ное с одним процессом, например закрытие файла процессом-потомком, не затра­
гивает таблиц файлов других процессов. В гл. 5 будет показано, что такое поведение
является типичным, но предок и потомок могут совместно использовать таблицу
файлов предка (как потоки обычно и делают).

Открытие файлов
Наиболее простой способ доступа к файлу связан с использованием системных
вызовов read() и write(). Однако прежде, чем к файлу можно будет получить доступ,
его требуется открыть с помощью системного вызова open() или creat(). Когда
работа с файлом закончена, его нужно закрыть посредством системного вызова
close().

Системный вызов open()
Открытие файла и получение файлового дескриптора осуществляются с помощью
системного вызова open():
#include
#include
#include
int open (const char *name, int flags);
int open (const char *name, int flags, mode_t mode);

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

56

Глава 2. Файловый ввод-вывод

позиции указывается его начало (нуль), и файл открывается для доступа в соответ­
ствии с заданными флагами (параметр flags).
Флаги для open(). Аргумент flags — это поразрядное ИЛИ, состоящее из од­
ного или нескольких флагов. Он должен указывать режим доступа, который может
иметь одно из следующих значений: O_RDONLY, O_WRONLY или O_RDWR. Эти аргументы
соответственно означают, что файл может быть открыт только для чтения, только
для записи или одновременно для того и другого.
Например, следующий код открывает каталог /home/kidd/madagascar для чтения:
int fd;
fd = open ("/home/kidd/madagascar", O_RDONLY);
if (fd == –1)
/* ошибка */

Если файл открыт только для чтения, в него невозможно что-либо записать, и на­
оборот. Процесс, осуществляющий системный вызов open(), должен иметь довольно
широкие права, чтобы получить запрашиваемый доступ. Например, если файл до­
ступен определенному пользователю только для чтения, то процесс, запущенный этим
пользователем, может открыть этот файл как O_RDONLY, но не как O_WRONLY или O_RDWR.
Перед указанием режима доступа задается вариант побитового «ИЛИ» для
аргумента flags. Вариант описывается одним или несколькими из следующих
значений, изменяющих поведение запроса на открытие файла.
 O_APPEND. Файл будет открыт в режиме дозаписи. Это означает, что перед каждым

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

зоваться только при работе с FIFO, каналами, сокетами и терминалами, но не
с обычными файлами.
 O_CLOEXEC. Задает флаг close-on-exec для открытого файла. Как только начнется

выполнение нового процесса, этот файл будет автоматически закрыт. Таким
образом, очевидна необходимость вызова fcntl() для задания флага и исклю­
чается возможность возникновения условий гонки. Этот флаг доступен лишь
в версии ядра Linux 2.6.23 и выше.
 O_CREAT. Если файл, обозначаемый именем name, не существует, то ядро создаст

его. Если же файл уже есть, то этот флаг не дает никакого эффекта (кроме слу­
чаев, в которых также задан O_EXCL).
 O_DIRECT. Файл будет открыт для непосредственного ввода-вывода.
 O_DIRECTORY. Если файл name не является каталогом, то вызов open() не удастся.
Этот флаг используется внутрисистемно библиотечным вызовом opendir().

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

57

 O_EXCL. При указании вместе с O_CREAT этот флаг предотвратит срабатывание
вызова open(), если файл с именем name уже существует. Данный флаг применя­
ется для предотвращения условий гонки при создании файлов. Если O_CREAT не

указан, то данный флаг не имеет значения.
 O_LARGEFILE. Указанный файл будет открыт с использованием 64-битных сме­

щений. Таким образом, становится возможно манипулировать файлами крупнее
2 Гбайт. Для таких операций требуется 64-битная архитектура.
 O_NOATIME+. Последнее значение времени доступа к файлу не обновляется при

его чтении. Этот флаг удобно применять при индексировании, резервном копи­
ровании и использовании других подобных программ, считывающих все файлы
в системе. Так предотвращается учет значительной записывающей активности,
возникающей при обновлении индексных дескрипторов каждого считываемого
файла. Этот флаг доступен лишь в версии ядра Linux 2.6.8 и выше.
 O_NOCTTY. Если файл с именем name указывает на терминальное устройство (на­

пример, /dev/tty), это устройство не получает контроля над процессом, даже
если в настоящий момент этот процесс не имеет контролирующего устройства.
Этот флаг используется редко.
 O_NOFOLLOW . Если name — это символьная ссылка, то вызов open() окончится

ошибкой. Как правило, происходит разрешение ссылки, после чего открывает­
ся целевой файл. Если остальные компоненты заданного пути также являются
ссылками, то вызов все равно удастся. Например, при name, равном /etc/ship/
plank.txt, вызов не удастся в случае, если plank.txt является символьной ссыл­
кой. При этом если plank.txt не является символьной ссылкой, а etc или ship
являются, то вызов будет выполнен успешно.
 O_NONBLOCK. Если это возможно, файл будет открыт в неблокирующем режиме.
Ни вызов open(), ни какая-либо иная операция не вызовут блокирования про­

цесса (перехода в спящий режим) при вводе-выводе. Такое поведение может
быть определено лишь для FIFO.
 O_SYNC. Файл будет открыт для синхронного ввода-вывода. Никакие операции

записи не завершатся, пока данные физически не окажутся на диске. Обычные
действия по считыванию уже протекают синхронно, поэтому данный флаг
никак не влияет на чтение. Стандарт POSIX дополнительно определяет O_DSYNC
и O_RSYNC, но в Linux эти флаги эквивалентны O_SYNC.
 O_TRUNC. Если файл уже существует, является обычным файлом и заданные для

него флаги допускают запись, то файл будет усечен до нулевой длины. Флаг
O_TRUNC для FIFO или терминального устройства игнорируется. Использование
с файлами других типов не определено. Указание O_TRUNC с O_RDONLY также явля­
ется неопределенным, так как для усечения файла вам требуется разрешение
на доступ к нему для записи.
Например, следующий код открывает для записи файл /home/teach/pearl. Если
файл уже существует, то он будет усечен до нулевой длины. Флаг O_CREAT не ука­
зывается, когда файл еще не существует, поэтому вызов не состоится:

58

Глава 2. Файловый ввод-вывод

int fd;
fd = open ("/home/teach/pearl", O_WRONLY | O_TRUNC);
if (fd == –1)
/* ошибка */

Владельцы новых файлов
Определить, какой пользователь является владельцем файла, довольно просто: uid
владельца файла является действительным uid процесса, создавшего файл.
Определить владеющую группу уже сложнее. Как правило, принято устанав­
ливать значение группы файла в значение действительного uid процесса, созда­
вшего файл. Такой подход практикуется в System V; вообще такая модель и такой
образ действия очень распространены в Linux и считаются стандартными.
Тем не менее операционная система BSD вносит здесь лишнее усложнение
и определяет собственный вариант поведения: группа файлов получает gid роди­
тельского каталога. Такое поведение можно обеспечить в Linux с помощью одного
из параметров времени монтирования1. Именно такое поведение будет срабатывать
в Linux по умолчанию, если для родительского каталога данного файла задан бит
смены индикатора группы (setgid). Хотя в большинстве систем Linux предпочита­
ется поведение System V (при котором новые файлы получают gid родительского
каталога), возможность поведения в стиле BSD (при котором новые файлы при­
обретают gid родительского каталога) подразумевает следующее: код, занятый
работой с владеющей группой нового файла, должен самостоятельно задать эту
группу посредством системного вызова fchown() (подробнее об этом — в гл. 8).
К счастью, вам нечасто придется заботиться о том, к какой группе принадлежит
файл.

Права доступа новых файлов
Обе описанные выше формы системного вызова open() допустимы. Аргумент mode
игнорируется, если файл не создается. Этот аргумент требуется, если задан флаг
O_CREAT. Если вы забудете указать аргумент mode при использовании O_CREAT, то резуль­
тат будет неопределенным и зачастую неприятным, поэтому лучше не забывайте.
При создании файла аргумент mode задает права доступа к этому новому файлу.
Режим доступа не проверяется при данном конкретном открытии файла, поэтому вы
можете выполнить операции, противоречащие присвоенным правам доступа, например
открыть для записи файл, для которого указаны права доступа только для чтения.
Аргумент mode является знакомой UNIX-последовательностью битов, регла­
ментирующей доступ. К примеру, он может представлять собой восьмеричное
значение 0644 (владелец может читать файл и записывать в него информацию, все
остальные — только читать). С технической точки зрения POSIX указывает, что
точные значения зависят от конкретной реализации. Соответственно, различные
1

Есть два параметра времени монтирования — bsdgroups или sysvgroups.

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

59

UNIX-подобные системы могут компоновать биты доступа по собственному
усмотрению. Однако во всех системах UNIX биты доступа реализованы одинаково,
поэтому пусть с технической точки зрения биты 0644 или 0700 и не являются пе­
реносимыми, они будут иметь одинаковый эффект в любой системе, с которой вы
теоретически можете столкнуться.
Тем не менее, чтобы компенсировать непереносимость позиций битов в режиме
доступа, POSIX предусматривает следующий набор констант, которые можно
указывать в виде двоичного «ИЛИ» и добавлять к аргументу mode:
 S_IRWXU — владелец имеет право на чтение, запись и исполнение файла;
 S_IRUSR — владелец имеет право на чтение;
 S_IWUSR — владелец имеет право на запись;
 S_IXUSR — владелец имеет право на исполнение;
 S_IRWXG — владеющая группа имеет право на чтение, запись и исполнение файла;
 S_IRGRP — владеющая группа имеет право на чтение;
 S_IWGRP — владеющая группа имеет право на запись;
 S_IXGRP — владеющая группа имеет право на исполнение;
 S_IRWXO — любой пользователь имеет право на чтение, запись и исполнение

файла;
 S_IROTH — любой пользователь имеет право на чтение;
 S_IWOTH — любой пользователь имеет право на запись;
 S_IXOTH — любой пользователь имеет право на исполнение.

Конкретные биты доступа, попадающие на диск, определяются с помощью двоич­
ного «И», объединяющего аргумент mode с пользовательской маской создания файла
(umask). Такая маска — это специфичный для процесса атрибут, обычно задаваемый
интерактивной оболочкой входа. Однако маску можно изменять с помощью вызова
umask(), позволяющего пользователю модифицировать права доступа, действующие
для новых файлов и каталогов. Биты пользовательской маски файла отключаются
в аргументе mode, сообщаемом вызову open(). Таким образом, обычная пользовательская
маска 022 будет преобразовывать значение 0666, сообщенное mode, в 0644. Вы, как сис­
темный программист, обычно не учитываете воздействия пользовательских масок,
когда задаете права доступа. Смысл подобной маски в том, чтобы сам пользователь
мог ограничить права доступа, присваиваемые программами новым файлам.
Рассмотрим пример. Следующий код открывает для записи файл, указанный
в file. Если файл не существует, то при действующей пользовательской маске 022
он создается с правами доступа 0644 (несмотря на то что в аргументе mode указано
значение 0664). Если он существует, то этот файл усекается до нулевой длины:
int fd;
fd = open (file, O_WRONLY | O_CREAT | O_TRUNC,
S_IWUSR | S_IRUSR | S_IWGRP | S_IRGRP | S_IROTH);
if (fd == –1)
/* ошибка */

60

Глава 2. Файловый ввод-вывод

Если мы готовы частично пожертвовать переносимостью (как минимум теоре­
тически) в обмен на удобочитаемость, то можем написать следующий код, функ­
ционально идентичный предыдущему:
int fd;
fd = open (file, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == –1)
/* ошибка */

Функция creat()
Комбинация O_WRONLY | O_CREAT | O_TRUNC настолько распространена, что существует
специальный системный вызов, обеспечивающий именно такое поведение:
#include
#include
#include
int creat (const char *name, mode_t mode);
ПРИМЕЧАНИЕ
Да, в названии этой функции не хватает буквы «e». Кен Томпсон (Ken Thompson), автор UNIX, както раз пошутил, что пропуск этой буквы был самым большим промахом, допущенным при создании
данной операционной системы.

Следующий типичный вызов creat():
int fd;
fd = creat (filename, 0644);
if (fd == –1)
/* ошибка */

идентичен такому:
int fd;
fd = open (filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == –1)
/* ошибка */

В большинстве архитектур Linux1 creat() является системным вызовом, хотя
его можно легко реализовать и в пользовательском пространстве:
int creat (const char *name, int mode)
{
return open (name, O_WRONLY | O_CREAT | O_TRUNC, mode);
}
1

Не забывайте, что системные вызовы определяются в зависимости от архитектуры. Та­
ким образом, в архитектуре x86-64 есть системный вызов creat(), а в Alpha — нет. Функ­
цию creat() можно, разумеется, использовать в любой архитектуре, но она может быть
реализована как библиотечная функция, а не как самостоятельный системный вызов.

Считывание с помощью read()

61

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

Возвращаемые значения и коды ошибок
При успешном вызове как open(), так и creat() возвращаемым значением является
дескриптор файла. При ошибке оба этих вызова возвращают –1 и устанавливают
errno в нужное значение ошибки (выше (см. гл. 1) подробно обсуждается errno
и перечисляются возможные значения ошибок). Обрабатывать ошибку при откры­
тии файла несложно, поскольку перед открытием обычно выполняется совсем мало
шагов, которые необходимо отменить (либо вообще не совершается никаких).
Типичный ответ — это предложение пользователю выбрать новое имя файла или
просто завершение программы.

Считывание с помощью read()
Теперь, когда вы знаете, как открывать файл, давайте научимся его читать, а в сле­
дующем разделе поговорим о записи.
Самый простой и распространенный механизм чтения связан с использованием
системного вызова read(), определенного в POSIX.1:
#include
ssize_t read (int fd, void *buf, size_t len);

Каждый вызов считывает не более len байт в памяти, на которые содержится
указание в buf. Считывание происходит с текущим значением смещения, в файле,
указанном в fd. При успешном вызове возвращается количество байтов, записанных
в buf. При ошибке вызов возвращает -1 и устанавливает errno. Файловая позиция
продвигается в зависимости от того, сколько байтов было считано с fd. Если объект,
указанный в fd, не имеет возможности позиционирования (например, это файл
символьного устройства), то считывание всегда начинается с «текущей» позиции.
Принцип использования прост. В данном примере информация считывается из
файлового дескриптора fd в word. Количество байтов, которые будут считаны, рав­
но размеру типа unsigned long, который (как минимум в Linux) имеет размер 4 байт
на 32-битных системах и 8 байт на 64-битных системах. При возврате nr содержит
количество считанных байтов или –1 при ошибке:
unsigned long word;
ssize_t nr;
/* считываем пару байт в 'word' из 'fd' */
nr = read (fd, &word, sizeof (unsigned long));
if (nr == –1)
/* ошибка */

62

Глава 2. Файловый ввод-вывод

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

Возвращаемые значения
Системный вызов read()может вернуть положительное ненулевое значение, мень­
шее чем len. Это может произойти по нескольким причинам: доступно меньше
байтов, чем указано в len, системный вызов прерван сигналом, конвейер оказался
поврежден (если fd ссылается на конвейер) и т. д.
Еще одна возможная проблема при использовании read() — получение возвра­
щаемого значения 0. Возвращая 0, системный вызов read() указывает конец файла
(end-of-file, EOF). Разумеется, в данном случае никакие байты считаны не будут.
EOF не считается ошибкой (соответственно, не дает возвращаемого значения –1).
Эта ситуация попросту означает, что файловая позиция превысила последнее до­
пустимое значение смещения в этом файле и читать больше нечего. Однако если
сделан вызов на считывание len байт, а необходимое количество байтов для счи­
тывания отсутствует, то вызов блокируется (переходит в спящий режим), пока
нужные байты не будут доступны. При этом предполагается, что файл был открыт
в неблокирующем режиме. Обратите внимание: эта ситуация отличается от воз­
врата EOF, то есть существует разница между «данные отсутствуют» и «достигнут
конец данных». В случае с EOF достигнут конец файла. При блокировании считы­
вающая функция будет дожидаться дополнительных данных — такие ситуации
возможны при считывании с сокета или файла устройства.
Некоторые ошибки исправимы. Например, если вызов read() прерван сигна­
лом еще до того, как было считано какое-либо количество байтов, то возвращает­
ся –1 (0 можно было бы спутать с EOF) и errno присваивается значение EINTR.
В таком случае вы можете и должны повторить считывание.
На самом деле последствия вызова read() могут быть разными.
 Вызов возвращает значение, равное len. Все len считанных байтов сохраняются
в buf. Именно это нам и требовалось.
 Вызов возвращает значение, меньшее чем len, но большее чем нуль. Считанные
байты сохраняются в buf. Это может случиться потому, что во время выполнения
считывания этот процесс был прерван сигналом. Ошибка возникает в середине
процесса, становится доступно значение, большее 0, но меньшее len. Конец
файла был достигнут ранее, чем было прочитано заданное количество байтов.
При повторном вызове (в котором соответствующим образом обновлены зна­
чения len и buf) оставшиеся байты будут считаны в оставшуюся часть буфера
либо укажут на причину проблемы.
 Вызов возвращает 0. Это означает конец файла. Считывать больше нечего.
 Вызов блокируется, поскольку в текущий момент данные недоступны. Этого не
происходит в неблокирующем режиме.

Считывание с помощью read()

63

 Вызов возвращает –1, а errno присваивается EINTR. Это означает, что сигнал был

получен прежде, чем были считаны какие-либо байты. Вызов будет повторен.
 Вызов возвращает –1, а errno присваивается EAGAIN. Это означает, что вызов

блокировался потому, что в настоящий момент нет доступных данных, и запрос
следует повторить позже. Это происходит только в неблокирующем режиме.
 Вызов возвращает –1, а errno присваивается иное значение, нежели EINTR или
EAGAIN. Это означает более серьезную ошибку. Простое повторение вызова в дан­

ном случае, скорее всего, не поможет.

Считывание всех байтов
Все описанные возможности подразумевают, что приведенный выше упрощенный
вариант использования read() не подходит, если вы желаете обработать все ошиб­
ки и действительно прочитать все байты до достижения len (или по крайней мере
до достижения конца файла). Для такого решения требуется применить цикл и не­
сколько условных операторов:
ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
if (ret == –1) {
if (errno == EINTR)
continue;
perror ("read");
break;
}
len -= ret;
buf += ret;
}

В этом фрагменте кода обрабатываются все пять условий. Цикл считывает len
байт с актуальной файловой позиции, равной значению fd, и записывает их в buf.
Разумеется, значение buf должно быть как минимум равно значению len. Чтение
продолжается, пока не будут получены все len байт или до достижения конца фай­
ла. Если прочитано ненулевое количество байтов, которое, однако, меньше len, то
значение len уменьшается на количество прочитанных байтов, buf увеличивается
на то же количество и вызов повторяется. Если вызов возвращает –1 и значение
errno, равное EINTR, то вызов повторяется без обновления параметров. Если вызов
возвращает –1 с любым другим значением errno, вызывается perror(). Он выводит
описание возникшей проблемы в стандартную ошибку, и выполнение цикла пре­
кращается.
Случаи частичного считывания не только допустимы, но и вполне обычны.
Из-за программистов, не озаботившихся правильной проверкой и обработкой
неполных операций считывания, возникают бесчисленные ошибки. Старайтесь
не пополнять их список!

64

Глава 2. Файловый ввод-вывод

Неблокирующее считывание
Иногда программист не собирается блокировать вызов read() при отсутствии до­
ступных данных. Вместо этого он предпочитает немедленный возврат вызова, ука­
зывающий, что данных действительно нет. Такой прием называется неблокирующим
вводом-выводом. Он позволяет приложениям выполнять ввод-вывод, потенциаль­
но применимый сразу ко многим файлам, вообще без блокирования, поэтому не­
достающие данные могут быть взяты из другого файла.
Следовательно, будет целесообразно проверять еще одно значение errno — EAGAIN.
Как было сказано выше, если определенный дескриптор файла был открыт в не­
блокирующем режиме (вызову open()сообщен флаг O_NONBLOCK) и данных для счи­
тывания не оказалось, то вызов read() возвратит –1 и вместо блокирования устано­
вит значение errno в EAGAIN. При выполнении неблокирующего считывания нужно
выполнять проверку на наличие EAGAIN, иначе вы рискуете перепутать серьезную
ошибку с тривиальным отсутствием данных. Например, вы можете использовать
примерно следующий код:
сhar buf[BUFSIZ];
ssize_t nr;
start:
nr = read (fd, buf, BUFSIZ);
if (nr == –1) {
if (errno == EINTR)
goto start; /* вот незадача */
if (errno == EAGAIN)
/* повторить вызов позже */
else
/* ошибка */
}
ПРИМЕЧАНИЕ
Если бы мы попытались обработать случай с EAGAIN так же, как и с EAGAIN (с применением goto
start), это практически не имело бы смысла. Мы могли бы и не применять неблокирующий вводвывод. Весь смысл использования неблокирующего ввода-вывода заключается в том, чтобы перехватить EAGAIN и выполнить другую полезную работу.

Другие значения ошибок
Другие коды относятся к ошибкам, допускаемым при программировании или (как
EIO) к низкоуровневым проблемам. Возможные значения errno после неуспешно­
го вызова read() таковы:
 EBADF — указанный дескриптор файла недействителен или не открыт для чте­

ния;
 EFAULT — указатель, предоставленный buf, не относится к адресному простран­

ству вызывающего процесса;

Запись с помощью write()

65

 EINVAL — дескриптор файла отображается на объект, не допускающий считы­

вания;
 EIO — возникла ошибка низкоуровневого ввода-вывода.

Ограничения размера для read()
Типы size_t и ssize_t types предписываются POSIX. Тип size_t используется для
хранения значений, применяемых для измерения размера в байтах. Тип ssize_t —
это вариант size_t, имеющий знак (отрицательные значения ssize_t используются
для дополнительной характеристики ошибок). В 32-битных системах базовыми
типами C для этих сущностей являются соответственно unsigned int и int. Эти два
типа часто используются вместе, поэтому потенциально более узкий диапазон
ssize_t лимитирует и размер size_t.
Максимальное значение size_t равно SIZE_MAX, максимальное значение ssize_t
составляет SSIZE_MAX. Если значение len превышает SSIZE_MAX, то результаты вызо­
ва read() не определены. В большинстве систем Linux значение SSIZE_MAX соответ­
ствует LONG_MAX, которое, в свою очередь, равно 2 147 483 647 на 32-битной машине.
Это относительно большое значение для однократного считывания, но о нем не­
обходимо помнить. Если вы использовали предыдущий считывающий цикл как
обобщенный суперсчитыватель, то, возможно, решите сделать нечто подобное:
if (len > SSIZE_MAX)
len = SSIZE_MAX;

При вызове read() с длиной, равной нулю, вызов немедленно вернется с возвра­
щаемым значением 0.

Запись с помощью write()
Самый простой и распространенный системный вызов для записи информации
называется write(). Это парный вызов для read(), он также определен в POSIX.1:
#include
ssize_t write (int fd, const void *buf, size_t count);

При вызове write() записывается некоторое количество байтов, меньшее или
равное тому, что указано в count. Запись начинается с buf, установленного в теку­
щую файловую позицию. Ссылка на нужный файл определяется по файловому
дескриптору fd. Если в основе файла лежит объект, не поддерживающий позицио­
нирования (так, в частности, выглядит ситуация с символьными устройствами),
запись всегда начинается с текущей позиции «курсора».
При успешном выполнении возвращается количество записанных байтов, а фай­
ловая позиция обновляется соответственно. При ошибке возвращается -1 и уста­
навливается соответствующее значение errno. Вызов write() может вернуть 0, но

66

Глава 2. Файловый ввод-вывод

это возвращаемое значение не имеет никакой специальной трактовки, а всего лишь
означает, что было записано 0 байт.
Как и в случае с read(), простейший пример использования тривиален:
const char *buf = "My ship is solid!";
ssize_t nr;
/* строка, находящаяся в 'buf', записывается в 'fd' */
nr = write (fd, buf, strlen (buf));
if (nr == –1)
/* ошибка */

Однако, как и в случае с read(), вышеуказанный код написан не совсем грамот­
но. Вызывающая сторона также должна проверять возможное наличие частичной
записи:
unsigned long word = 1720;
size_t count;
ssize_t nr;
count = sizeof (word);
nr = write (fd, &word, count);
if (nr == –1)
/* ошибка, проверить errno */
else if (nr != count)
/* возможна ошибка, но значение 'errno' не установлено */

Случаи частичной записи
Системный вызов write() выполняет частичную запись не так часто, как системный
вызов read() — частичное считывание. Кроме того, в случае с write() отсутствует
условие EOF. В случае с обычными файлами write() гарантированно выполняет
всю запрошенную запись, если только не возникает ошибка.
Следовательно, при записи информации в обычные файлы не требуется исполь­
зовать цикл. Однако при работе с файлами других типов, например сокетами, цикл
может быть необходим. С его помощью можно гарантировать, что вы действительно записали все требуемые байты. Еще одно достоинство использования цикла
заключается в том, что второй вызов write() может вернуть ошибку, проясняющую,
по какой причине при первом вызове удалось осуществить лишь частичную запись
(правда, вновь следует оговориться, что такая ситуация не слишком распростра­
нена). Вот пример:
ssize_t ret, nr;
while (len != 0 && (ret = write (fd, buf, len)) != 0) {
if (ret == –1) {
if (errno == EINTR)
continue;
perror ("write");

Запись с помощью write()

67

break;
}
len -= ret;
buf += ret;
}

Режим дозаписи
Когда дескриптор fd открывается в режиме дозаписи (с флагом O_APPEND), запись
начинается не с текущей позиции дескриптора файла, а с точки, в которой в данный
момент находится конец файла.
Предположим, два процесса пытаются записать информацию в конец одного
и того же файла. Такая ситуация распространена: например, она может возникать
в журнале событий, разделяемом многими процессами. Перед началом работы
файловые позиции установлены правильно, каждая из них соответствует концу
файла. Первый процесс записывает информацию в конец файла. Если режим до­
записи не используется, то второй процесс, попытавшись сделать то же самое,
начнет записывать свои данные уже не в конце файла, а в точке с тем смещением,
где конец файла находился до операции записи, выполненной первым процессом.
Таким образом, множественные процессы не могут дозаписывать информацию в ко­
нец одного и того же файла безявной синхронизации между ними, поскольку при
ее отсутствии наступают условия гонки.
Режим дозаписи избавляет нас от таких неудобств. Он гарантирует, что фай­
ловая позиция всегда устанавливается в его конец, поэтому все добавляемые
информационные фрагменты всегда дозаписываются правильно, даже если они
поступают от нескольких записывающих процессов. Эту ситуацию можно срав­
нить с атомарным обновлением файловой позиции, предшествующим каждому
запросу на запись информации. Затем файловая позиция обновляется и устанав­
ливается в точку, соответствующую окончанию последних записанных данных.
Это совершенно не мешает работе следующего вызова write(), поскольку он об­
новляет файловую позицию автоматически, но может быть критичным, если по
какой-то причине далее последует вызов read(), а не write().
При решении определенных задач режим дозаписи целесообразен, например,
при упомянутой выше операции записи файлов журналов, но в большинстве слу­
чаев он не находит применения.

Неблокирующая запись
Когда дескриптор fd открывается в неблокирующем режиме (с флагом O_NONBLOCK),
а запись в том виде, в котором она выполнена, в нормальных условиях должна быть
заблокирована, системный вызов write() возвращает –1 и устанавливает errno в зна­
чение EAGAIN. Запрос следует повторить позже. С обычными файлами этого, как
правило, не происходит.

68

Глава 2. Файловый ввод-вывод

Другие коды ошибок
Следует отдельно упомянуть следующие значения errno.
 EBADF — указанный дескриптор файла недопустим или не открыт для записи.
 EFAULT — указатель, содержащийся в buf, ссылается на данные, расположенные

вне адресного пространства процесса.
 EFBIG — в результате записи файл превысил бы максимум, допустимый для

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

для записи.
 EIO — произошла ошибка низкоуровневого ввода-вывода.
 ENOSPC — файловая система, к которой относится указанный дескриптор файла,

не обладает достаточным количеством свободного пространства.
 EPIPE — указанный дескриптор файла ассоциирован с конвейером или сокетом,
чей считывающий конец закрыт. Этот процесс также получит сигнал SIGPIPE.
Стандартное действие, выполняемое при получении сигнала SIGPIPE, — завер­

шение процесса-получателя. Следовательно, такие процессы получают данное
значение errno, только когда они явно запрашивают, как поступить с этим сиг­
налом — игнорировать, блокировать или обработать его.

Ограничения размера при использовании write()
Если значение count превышает SSIZE_MAX, то результат вызова write() не определен.
При вызове write() со значением count, равным нулю, вызов возвращается не­
медленно и при этом имеет значение 0.

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

Запись с помощью write()

69

содержимое на диск (этот процесс называется отложенной записью). Таким обра­
зом, вызовы write работают быстро и возвращаются практически без задержки.
Кроме того, ядро может откладывать такие операции записи на сравнительно не­
активные периоды и объединять в «пакеты» несколько отложенных записей.
Подобная запись с отсрочкой не меняет семантики POSIX. Например, если
выполняется вызов для считывания только что записанных данных, находящихся
в буфере, но отсутствующих на диске, в ответ на запрос поступает именно инфор­
мация из буфера, а не «устаревшие» данные с диска. Такое поведение, в свою оче­
редь, повышает производительность, поскольку в ответ на запрос о считывании
поступают данные из хранимого в памяти кэша, а диск вообще не участвует в опе­
рации. Запросы о чтении и записи чередуются верно, а мы получаем ожидаемый
результат — конечно, если система не откажет прежде, чем данные окажутся на
диске! При аварийном завершении системы наша информация на диск так и не
попадет, пусть приложение и будет считать, что запись прошла успешно.
Еще одна проблема, связанная с отложенной записью, заключается в невозмож­
ности принудительного упорядочения записи. Конечно, приложению может требо­
ваться, чтобы запросы записи обрабатывались именно в том порядке, в котором они
попадают на диск. Однако ядро может переупорядочивать запросы записи так, как
считает целесообразным в первую очередь для оптимизации производительности.
Как правило, это несоответствие приводит к проблемам лишь в случае аварийного
завершения работы системы, поскольку в нормальном рабочем процессе содержимое
всех буферов рано или поздно попадает в конечную версию файла, содержащуюся на
диске, — отложенная запись срабатывает правильно. Абсолютное большинство при­
ложений никак не регулируют порядок записи. На практике упорядочение записи
применяется редко, наиболее распространенные примеры подобного рода связаны
с базами данных. В базах данных важно обеспечить порядок операций записи, при
котором база данных гарантированно не окажется в несогласованном состоянии.
Последнее неудобство, связанное с использованием отложенной записи, — это
сообщения системы о тех или иных ошибках ввода-вывода. Любая ошибка вводавывода, возникающая при отложенной записи, — допустим, отказ физического
диска — не может быть сообщена процессу, который сделал вызов о записи. На са­
мом деле грязные буферы, расположенные в ядре, вообще никак не ассоциированы
с процессами. Данные, находящиеся в конкретном грязном буфере, могли быть
доставлены туда несколькими процессами, причем выход процесса может произой­
ти как раз в промежуток времени, когда данные уже записаны в буфер, но еще не
попали на диск. Кроме того, как в принципе можно сообщить процессу о неуспеш­
ной записи уже постфактум?
Учитывая все потенциальные проблемы, которые могут возникать при отло­
женной записи, ядро стремится минимизировать связанные с ней риски. Чтобы
гарантировать, что все данные будут своевременно записаны на диск, ядро задает
максимальный возраст буфера и переписывает все данные из грязных буферов до
того, как их срок жизни превысит это значение. Пользователь может сконфигури­
ровать данное значение в /proc/sys/vm/dirty_expire_centisecs. Значение указывается
в сантисекундах (сотых долях секунды).

70

Глава 2. Файловый ввод-вывод

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

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

fsync() и fdatasync()
Простейший метод, позволяющий гарантировать, что данные окажутся на диске,
связан с использованием системного вызова fsync(). Этот вызов стандартизирован
в POSIX.1b:
#include
int fsync (int fd);

Вызов fsync() гарантирует, что все грязные данные, ассоциированные с кон­
кретным файлом, на который отображается дескриптор fd, будут записаны на диск.
Файловый дескриптор fd должен быть открыт для записи. В ходе отложенной за­
писи вызов заносит на диск как данные, так и метаданные. К метаданным относят­
ся, в частности, цифровые отметки о времени создания файла и другие атрибуты,
содержащиеся в индексном дескрипторе. Вызов fsync() не вернется, пока жесткий
диск не сообщит, что все данные и метаданные оказались на диске.
В настоящее время существуют жесткие диски с кэшами (обратной) записи,
поэтому вызов fsync() не может однозначно определить, оказались ли данные на
диске физически к определенному моменту. Жесткий диск может сообщить, что
данные были записаны на устройство, но на самом деле они еще могут находиться
в кэше записи этого диска. К счастью, все данные, которые оказываются в этом
кэше, должны отправляться на диск в срочном порядке.
В Linux присутствует и системный вызов fdatasync():
#include
int fdatasync (int fd);

Синхронизированный ввод-вывод

71

Этот системный вызов функционально идентичен fsync(), с оговоркой, что
он лишь сбрасывает на диск данные и метаданные, которые потребуются для
корректного доступа к файлу в будущем. Например, после вызова fdatasync() на
диске окажется информация о размере файла, так как она необходима для вер­
ного считывания файла. Этот вызов не гарантирует, что несущественные мета­
данные будут синхронизированы с диском, поэтому потенциально он быстрее,
чем fsync(). В большинстве практических случаев метаданные (например, от­
метка о последнем изменении файла) не считаются существенной частью транз­
акции, поэтому бывает достаточно применить fdatasync() и получить выигрыш
в скорости.
ПРИМЕЧАНИЕ
При вызове fsync() всегда выполняется как минимум две операции ввода-вывода: в ходе одной из
них осуществляется отложенная запись измененных данных, а в ходе другой обновляется временная метка изменения индексного дескриптора. Данные из индексного дескриптора и данные, относящиеся к файлу, могут находиться в несмежных областях диска, поэтому может потребоваться
затратная операция позиционирования. Однако в большинстве случаев, когда основная задача
сводится к верной передаче транзакции, можно не включать в эту транзакцию метаданные, несущественные для правильного доступа к файлу в будущем. Примером таких метаданных является
отметка о последнем изменении файла. По этой причине в большинстве случаев вызов fdatasync()
является допустимым, а также обеспечивает выигрыш в скорости.

Обе функции используются одинаковым простым способом:
int ret;
ret = fsync (fd);
if (ret == –1)
/* ошибка */

Вот пример с использованием fdatasync():
int ret;
/* аналогично fsync, но на диск не сбрасываются несущественные метаданные */
ret = fdatasync (fd);
if (ret == –1)
/* ошибка */

Ни одна из этих функций не гарантирует, что все обновившиеся записи ката­
логов, в которых содержится файл, будут синхронизированы на диске. Имеется
в виду, что если ссылка на файл недавно была обновлена, то информация из дан­
ного файла может успешно попасть на диск, но еще не отразиться в ассоциирован­
ной с файлом записи из того или иного каталога. В таком случае файл окажется
недоступен. Чтобы гарантировать, что на диске окажутся и все обновления, каса­
ющиеся записей в каталогах, fsync() нужно вызвать и к дескриптору файла, откры­
тому для каталога, содержащего интересующий нас файл.
Возвращаемые значения и коды ошибок. В случае успеха оба вызова возвра­
щают 0. В противном случае оба вызова возвращают –1 и устанавливают errno в одно
из следующих трех значений:

72

Глава 2. Файловый ввод-вывод

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

открытым для записи;
 EINVAL — указанный дескриптор файла отображается на объект, не поддержи­

вающий синхронизацию;
 EIO — при синхронизации произошла низкоуровневая ошибка ввода-вывода;

здесь мы имеем дело с реальной ошибкой ввода-вывода, более того — именно
тут обычно отлавливаются подобные ошибки.
В некоторых версиях Linux вызов fsync() может оказаться неуспешным потому,
что вызов fsync() не реализован в базовой файловой системе этой версии, даже
если fdatasync() реализован. Некоторые параноидальные приложения пытаются
сделать вызов fdatasync(), если fsync() вернул EINVAL, например:
if (fsync (fd) == –1) {
/*
* Предпочтителен вариант сfsync(), но мы пытаемся сделать и fdatasync(),
* если fsync() окажется неуспешным — так, на всякий случай.
*/
if (errno == EINVAL) {
if (fdatasync (fd) == –1)
perror ("fdatasync");
} else
perror ("fsync");
}

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

sync()
Дедовский системный вызов sync() не оптимален для решения описываемых задач,
зато гораздо более универсален. Этот вызов обеспечивает синхронизацию всех
буферов, имеющихся на диске:
#include
void sync (void);

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

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

Синхронизированный ввод-вывод

73

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

Флаг O_SYNC
Флаг O_SYNC может быть передан вызову open(). Этот флаг означает, что все операции
ввода-вывода, осуществляемые с этим файлом, должны быть синхронизированы:
int fd;
fd = open (file, O_WRONLY | O_SYNC);
if (fd == –1) {
perror ("open");
return –1;
}

Запросы на считывание всегда синхронизированы. Если бы такая синхрониза­
ция отсутствовала, то мы не могли бы судить о допустимости данных, считанных
из предоставленного буфера. Тем не менее, как уже упоминалось выше, вызовы
write(), как правило, не синхронизируются. Нет никакой связи между возвратом
вызова и отправкой данных на диск. Флаг O_SYNC принудительно устанавливает
такую связь, гарантируя, что вызовы write() будут выполнять синхронизированный
ввод-вывод.
Флаг O_SYNC можно рассмотреть в следующем ключе: он принудительно выпол­
няет неявный вызов fsync() после каждой операции write() перед возвратом вызо­
ва. Этот флаг обеспечивает именно такую семантику, хотя ядро реализует вызов
O_SYNC немного эффективнее.
При использовании O_SYNC несколько ухудшаются два показателя операций
записи: время, затрачиваемое ядром, и пользовательское время. Это соответствен­
но периоды, затраченные на работу в пространстве ядра и в пользовательском
пространстве. Более того, в зависимости от размера записываемого файла O_SYNC
общее истекшее время также может увеличиваться на один-два порядка, посколь­
ку все время ожидания при вводе-выводе (время, необходимое для завершения
операций ввода-вывода) суммируется со временем, затрачиваемым на работу про­
цесса. Налицо огромное увеличение издержек, поэтому синхронизированный
ввод-вывод следует использовать только при отсутствии альтернатив.

74

Глава 2. Файловый ввод-вывод

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

Флаги O_DSYNC и O_RSYNC
Стандарт POSIX определяет еще два флага для вызова open(), связанных с синхро­
низированным вводом-выводом, — O_DSYNC и O_RSYNC. В Linux эти флаги определя­
ются как синонимичные O_SYNC, они предоставляют аналогичное поведение.
Флаг O_DSYNC указывает, что после каждой операции должны синхронизировать­
ся только обычные данные, но не метаданные. Ситуацию можно сравнить с неявным
вызовом fdatasync() после каждого запроса на запись. O_SYNC предоставляет более
надежные гарантии, поэтому совмещение O_DSYNC с ним не влечет за собой никако­
го функционального ухудшения. Возможно лишь потенциальное снижение произ­
водительности, связанное с тем, что O_SYNC предъявляет к системе более строгие
требования.
Флаг O_RSYNC требует синхронизации запросов как на считывание, так и на запись.
Его нужно использовать вместе с O_SYNC или O_DSYNC. Как было сказано выше, опе­
рации считывания синхронизируются изначально — если уж на то пошло, они не
возвращаются, пока получат какую-либо полезную информацию, которую можно
будет предоставить пользователю. Флаг O_RSYNC регламентирует, что все побочные
эффекты операции считывания также должны быть синхронизированы. Это озна­
чает, что обновления метаданных, происходящие в результате считывания, должны
быть записаны на диск прежде, чем вернется вызов. На практике данное требование
обычно всего лишь означает, что до возврата вызова read() должно быть обновлено
время доступа к файлу, фиксируемое в копии индексного дескриптора, находящей­
ся на диске. В Linux флаг O_RSYNC определяется как аналогичный O_SYNC, пусть это
и кажется нецелесообразным (ведь O_RSYNC не являются подмножеством O_SYNC, в от­
личие от O_DSYNC, которые таким подмножеством являются). В настоящее время
в Linux отсутствует способ, позволяющий обеспечить функциональность O_RSYNC.
Максимум, что может сделать разработчик, — инициировать fdatasync() после
каждого вызова read(). Правда, такое поведение требуется редко.

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

Закрытие файлов

75

не оправдывает затрачиваемых на нее усилий. Вероятно, инструменты, которые уже
доступны вам на уровне операционной системы, позволят обеспечить значительно
более высокую производительность, чем подобные им существующие на уровне
приложений. Тем не менее в системах баз данных обычно предпочтительнее исполь­
зовать собственный механизм кэширования и свести к минимуму участие операци­
онной системы в рабочих процессах, насколько это возможно.
Когда мы снабжаем вызов open() флагом O_DIRECT, мы предписываем ядру свести
к минимуму активность управления вводом-выводом. При наличии этого флага
операции ввода-вывода будут инициироваться непосредственно из буферов поль­
зовательского пространства на устройство, минуя страничный кэш. Все операции
ввода-вывода станут синхронными, вызовы не будут возвращаться до завершения
этих действий.
При выполнении непосредственного ввода-вывода длина запроса, выравнивание
буфера и смещения файлов должны представлять собой целочисленные значения,
кратные размеру сектора на базовом устройстве. Как правило, размер сектора со­
ставляет 512 байт. До выхода версии ядра Linux 2.6 это требование было еще стро­
же. Так, в версии 2.4 все эти значения должны были быть кратны размеру логиче­
ского блока файловой системы (обычно 4 Кбайт). Для обеспечения совместимости
приложения должны соответствовать более крупной (и потенциально менее удоб­
ной) величине — размеру логического блока.

Закрытие файлов
После того как программа завершит работу с дескриптором файла, она может
разорвать связь, существующую между дескриптором и файлом, который с ним
ассоциирован. Это делается с помощью системного вызова close():
#include
int close (int fd);

Вызов close() отменяет отображение открытого файлового дескриптора fd
и разрывает связь между файлом и процессом. Данный дескриптор файла больше
не является допустимым, и ядро свободно может переиспользовать его как возвра­
щаемое значение для последующих вызовов open() или creat(). При успешном
выполнении вызов close() возвращает 0. При ошибке он возвращает –1 и устанав­
ливает errno в соответствующее значение. Пример использования прост:
if (close (fd) == –1)
perror ("close");

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

76

Глава 2. Файловый ввод-вывод

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

Значения ошибок
Распространена порочная практика — не проверять возвращаемое значение close().
В результате можно упустить критическое условие, приводящее к ошибке, так как
подобные ошибки, связанные с отложенными операциями, могут не проявиться
вплоть до момента, как о них сообщит close().
При таком отказе вы можете встретить несколько возможных значений errno.
Кроме EBADF (заданный дескриптор файла оказался недопустимым), наиболее важ­
ным значением ошибки является EIO. Оно соответствует низкоуровневой ошибке
ввода-вывода, которая может быть никак не связана с самим актом закрытия. Если
файловый дескриптор допустим, то при выдаче сообщения об ошибке он всегда
закрывается, независимо от того, какая именно ошибка произошла. Ассоциирован­
ные с ним структуры данных высвобождаются.
Вызов close() никогда не возвращает EINTR, хотя POSIX это допускает. Разра­
ботчики ядра Linux знают, что делают.

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

Позиционирование с помощью Iseek()

77

#include
#include
off_t lseek (int fd, off_t pos, int origin);

Поведение вызова lseek() зависит от аргумента origin, который может иметь
одно из следующих значений.
 SEEK_CUR — текущая файловая позиция дескриптора fd установлена в его текущее
значение плюс pos. Последний может иметь отрицательное, положительное или
нулевое значение. Если pos равен нулю, то возвращается текущее значение

файловой позиции.
 SEEK_END — текущая файловая позиция дескриптора fd установлена в текущее
значение длины файла плюс pos, который может иметь отрицательное, положи­
тельное или нулевое значение. Если pos равен нулю, то смещение устанавлива­

ется в конец файла.
 SEEK_SET — текущая файловая позиция дескриптора fd установлена в pos. Если
pos равен нулю, то смещение устанавливается в начало файла.

В случае успеха этот вызов возвращает новую файловую позицию. При ошибке
он возвращает –1 и присваивает errno соответствующее значение.
В следующем примере файловая позиция дескриптора fd получает значение
1825:
off_t ret;
ret = lseek (fd, (off_t) 1825, SEEK_SET);
if (ret == (off_t) –1)
/* ошибка */

В качестве альтернативы можно установить файловую позицию дескриптора
fd в конец файла:
off_t ret;
ret = lseek (fd, 0, SEEK_END);
if (ret == (off_t) –1)
/* ошибка */

Вызов lseek() возвращает обновленную файловую позицию, поэтому его мож­
но использовать для поиска текущей файловой позиции. Нужно установить зна­
чение SEEK_CUR в нуль:
int pos;
pos = lseek (fd, 0, SEEK_CUR);
if (pos == (off_t) –1)
/* ошибка */
else
/* 'pos' — это текущая позиция fd */

78

Глава 2. Файловый ввод-вывод

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

Поиск с выходом за пределы файла
Можно указать lseek() переставить указатель файловой позиции за пределы фай­
ла (дальше его конечной точки). Например, следующий код устанавливает позицию
на 1688 байт после конца файла, на который отображается дескриптор fd:
int ret;
ret = lseek (fd, (off_t) 1688, SEEK_END);
if (ret == (off_t) –1)
/* ошибка */

Само по себе позиционирование с выходом за пределы файла не дает результа­
та — запрос на считывание такой новой файловой позиции вернет значение EOF
(конец файла). Однако если затем сделать запрос на запись, указав такую конеч­
ную позицию, то между старым и новым значениями длины файла будет создано
дополнительное пространство, которое программа заполнит нулями.
Такое заполнение нулями называется дырой. В UNIX-подобных файловых сис­
темах дыры не занимают на диске никакого пространства. Таким образом, общий
размер всех файлов, содержащихся в файловой системе, может превышать физиче­
ский размер диска. Файлы, содержащие дыры, называются разреженными. При ис­
пользовании разреженных файлов можно экономить значительное пространство
на диске, а также оптимизировать производительность, ведь при манипулировании
дырами не происходит никакого физического ввода-вывода.
Запрос на считывание фрагмента файла, полностью находящегося в пределах
дыры, вернет соответствующее количество нулей.
Значения ошибок. При ошибке lseek() возвращает –1 и присваивает errno одно
из следующих значений.
 EBADF — указанное значение дескриптора не ссылается на открытый файловый

дескриптор.
 EINVAL — значение аргумента origin не является SEEK_SETSEEK_CUR или SEEK_END либо
результирующая файловая позиция получится отрицательной. Факт, что EINVAL

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

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

поддерживает позиционирования, например с конвейером, FIFO или сокетом.

Позиционное чтение и запись

79

Ограничения
Максимальные значения файловых позиций ограничиваются типом off_t. В боль­
шинстве машинных архитектур он определяется как тип long языка C. В Linux
размер этого вида обычно равен размеру машинного слова. Как правило, под разме­
ром машинного слова понимается размер универсальных аппаратных регистров
в конкретной архитектуре. Однако на внутрисистемном уровне ядро хранит фай­
ловые смещения в типах long языка C. На машинах с 64-битной архитектурой это
не представляет никаких проблем, но такая ситуация означает, что на 32-битных
машинах ошибка EOVERFLOW может возникать при выполнении операций относи­
тельного поиска.

Позиционное чтение и запись
Linux позволяет использовать вместо lseek() два варианта системных вызовов —
read() и write(). Оба эти вызова получают файловую позицию, с которой требует­
ся начинать чтение или запись. По завершении работы эти вызовы не обновляют
позицию файла.
Данная форма считывания называется pread():
#define _XOPEN_SOURCE 500
#include
ssize_t pread (int fd, void *buf, size_t count, off_t pos);

Этот вызов считывает до count байт в buf, начиная от файлового дескриптора fd
на файловой позиции pos.
Данная форма записи называется pwrite():
#define _XOPEN_SOURCE 500
#include
ssize_t pwrite (int fd, const void *buf, size_t count, off_t pos);

Этот вызов записывает до count байт в buf, начиная от файлового дескриптора
fd на файловой позиции pos.

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

80

Глава 2. Файловый ввод-вывод

вызов lseek(), но с тремя оговорками. Во-первых, позиционные вызовы проще в ис­
пользовании, особенно если нас интересует какая-либо хитрая манипуляция, напри­
мер перемещение по файлу в обратном направлении или произвольном порядке.
Во-вторых, завершив работу, они не обновляют указатель на файл. Наконец, самое
важное — они исключают возможность условий гонки, которые могут возникнуть при
использовании lseek().
Потоки совместно используют файловые таблицы, и текущая файловая позиция
хранится в такой разделяемой таблице, поэтому один поток программы может обно­
вить файловую позицию уже после вызова lseek(), поступившего к файлу от друго­
го потока, но прежде, чем закончится выполнение операции считывания или записи.
Налицо потенциальные условия гонки, если в вашей программе присутствуют два
и более потока, оперирующие одним и тем же файловым дескриптором. Таких усло­
вий гонки можно избежать, работая с системными вызовами pread() и pwrite().
Значения ошибок. В случае успеха оба вызова возвращают количество байтов,
которые соответственно были прочитаны или записаны. Возвращаемое значение 0,
полученное от pread(), означает конец файла; возвращаемое значение 0 от pwrite()
указывает, что вызов ничего не записал. При ошибке оба вызова возвращают –1
и устанавливают errno соответствующее значение. В случае pread() это может быть
любое допустимое значение errno для read() или lseek(). В случае pwrite() это мо­
жет быть любое допустимое значение errno для write() или lseek().

Усечение файлов
В Linux предоставляется два системных вызова для усечения длины файла. Оба
они определены и обязательны (в той или иной степени) согласно различным
стандартам POSIX. Вот эти вызовы:
#include
#include
int ftruncate (int fd, off_t len);

и
#include
#include
int truncate (const char *path, off_t len);

Оба системных вызова выполняют усечение заданного файла до длины, указан­
ной в len. Системный вызов ftruncate() оперирует файловым дескриптором fd,
который должен быть открыт для записи. Системный вызов truncate() оперирует
именем файла, указанным в path, причем этот файл должен быть пригоден для за­
писи. Оба вызова при успешном выполнении возвращают 0. При ошибке оба вызо­
ва возвращают –1 и присваивают errno соответствующее значение.
Как правило, эти системные вызовы используются для усечения файла до дли­
ны, меньшей чем текущая. При успешном возврате вызова длина файла равна len.

Мультиплексный ввод-вывод

81

Все данные, прежде находившиеся между len и неусеченным показателем длины,
удаляются и становятся недоступны для запросов на считывание.
Эти функции могут также выполнять «усечение» файла с увеличением его
размера, как при комбинации позиционирования и записи, описанной выше
(см. разд. «Позиционирование с выходом за пределы файла» данной главы).
Дополнительные байты заполняются нулями.
Ни при одной из этих операций файловая позиция не обновляется.
Рассмотрим, например, файл pirate.txt длиной 74 байт со следующим содер­
жимым:
Edward Teach was a notorious English pirate.
He was nicknamed Blackbeard

Не выходя из каталога с этим файлом, запустим следующую программу:
#include
#include
int main()
{
int ret;
ret = truncate
if (ret == –1)
perror
return
}

("./pirate.txt", 45);
{
("truncate");
–1;

return 0;
}

В результате получаем файл следующего содержания длиной 45 байт:
Edward Teach was a notorious English pirate.

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

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

82

Глава 2. Файловый ввод-вывод

Не прибегая к потокам — в сущности, обслуживая каждый файловый дескрип­
тор отдельно, — одиночный процесс, разумеется, может фиксироваться только на
одном дескрипторе в каждый момент времени. Работать с множественными фай­
ловыми дескрипторами удобно, если они всегда готовы к считыванию или записи.
Однако если программа встретит файловый дескриптор, который еще не готов
к взаимодействию (допустим, мы выполнили системный вызов read(), а данные
для считывания пока отсутствуют), то процесс блокируется и не сможет заняться
работой с какими-либо другими файловыми дескрипторами. Он может блокиро­
ваться даже на несколько секунд, из-за чего приложение станет неэффективным
и будет только раздражать пользователя. Более того, если нужные для файлового
дескриптора данные так и не появятся, то блокировка может длиться вечно. Опе­
рации ввода-вывода, связанные с различными файловыми дескрипторами, зачастую
взаимосвязаны (вспомните, например, работу с конвейерами), поэтому один из
файловых дескрипторов вполне может оставаться не готовым к работе, пока не
будет обслужен другой. В частности, при работе с сетевыми приложениями, в ко­
торых одновременно бывает открыто большое количество сокетов, эта проблема
может стать весьма серьезной.
Допустим, произошла блокировка на файловом дескрипторе, относящемся
к межпроцессному взаимодействию. В то же время в режиме ожидания остаются
данные, введенные с клавиатуры (stdin). Пока блокированный файловый дескрип­
тор, отвечающий за межпроцессное взаимодействие, не вернет данные, приложение
так и не узнает, что еще остаются необработанные данные с клавиатуры. Однако
что делать, если возврата от блокированной операции так и не произойдет?
Ранее в данной главе мы обсуждали неблокирующий ввод-вывод в качестве воз­
можного решения этой проблемы. Приложения, работающие в режиме неблокиру­
ющего ввода-вывода, способны выдавать запросы на ввод-вывод, которые в случае
подвисания не блокируют всю работу, а возвращают особое условие ошибки. Это ре­
шение неэффективно по двум причинам. Во-первых, процессу приходится постоянно
осуществлять операции ввода-вывода в каком-то произвольном порядке, дожидаясь,
пока один из открытых файловых дескрипторов не будет готов выполнить операцию
ввода-вывода. Это некачественная конструкция программы. Во-вторых, программа
работала бы эффективнее, если бы могла ненадолго засыпать, высвобождая процессор
для решения других задач. Просыпаться программа должна, только когда один фай­
ловый дескриптор или более будут готовы к обработке ввода-вывода.
Пора познакомиться с мультиплексным вводом-выводом. Мультиплексный вводвывод позволяет приложениям параллельно блокировать несколько файловых
дескрипторов и получать уведомления, как только любой из них будет готов к чте­
нию или записи без блокировки, поэтому мультиплексный ввод-вывод оказывает­
ся настоящим стержнем приложения, выстраиваемым примерно по следующему
принципу.
1. Мультиплексный ввод-вывод: сообщите мне, когда любой из этих файловых
дескрипторов будет готов к операции ввода-вывода.
2. Ни один не готов? Перехожу в спящий режим до готовности одного или не­
скольких дескрипторов.

Мультиплексный ввод-вывод

83

3. Проснулся! Где готовый дескриптор?
4. Обрабатываю без блокировки все файловые дескрипторы, готовые к вводу-вы­
воду.
5. Возвращаюсь к шагу 1.
В Linux предоставляется три сущности для различных вариантов мультиплекс­
ного ввода-вывода. Это интерфейсы для выбора (select), опроса (poll) и расши­
ренного опроса (epoll). Здесь мы рассмотрим первые два решения. Последний
вариант — продвинутый, специфичный для Linux. Его мы обсудим в гл. 4.

select()
Системный вызов select() обеспечивает механизм для реализации синхронного
мультиплексного ввода-вывода:
#include
int select (int n,
fd_set
fd_set
fd_set
struct

*readfds,
*writefds,
*exceptfds,
timeval *timeout);

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

Вызов к select() блокируется, пока указанные файловые дескрипторы не будут
готовы к выполнению ввода-вывода либо пока не истечет необязательный интервал
задержки.
Отслеживаемые файловые дескрипторы делятся на три группы. Дескрипторы
из каждой группы дожидаются событий определенного типа. Файловые дескрип­
торы, перечисленные в readfds, отслеживают, не появились ли данные, доступные
для чтения, то есть они проверяют, удастся ли совершить операцию считывания
без блокировки. Файловые дескрипторы, перечисленные в группе writefds, анало­
гичным образом дожидаются возможности совершить неблокирующую операцию
записи. Наконец, файловые дескрипторы из группы exceptfds следят, не было ли
исключения либо не появились ли в доступе внеполосные данные (в таком состоя­
нии могут находиться только сокеты). Одна из групп может иметь значение NULL;
это означает, что select() не отслеживает события данного вида.
При успешном возврате каждая группа изменяется таким образом, что в ней
остаются только дескрипторы, готовые к вводу-выводу определенного типа, соот­
ветствующего конкретной группе. Предположим, у нас есть два файловых дес­
криптора со значениями 7 и 9, которые относятся к группе readfds. Возвращается
вызов. Если к этому моменту дескриптор 7 не покинул эту группу, то, следователь­
но, он готов к неблокирующему считыванию. Если 9 уже не находится в этой

84

Глава 2. Файловый ввод-вывод

группе, то он, вероятно, не сможет выполнить считывание без блокировки. Под
«вероятно» здесь подразумевается, что данные для считывания могли стать до­
ступны уже после того, как произошел возврат вызова. В таком случае при после­
дующем вызове select() этот файловый дескриптор будет расцениваться как го­
товый для считывания1.
Первый параметр, n, равен наивысшему значению файлового дескриптора, при­
сутствующему во всех группах, плюс 1. Следовательно, сторона, вызывающая select(),
должна проверить, какой из заданных файловых дескрипторов имеет наивысшее
значение, а затем передать сумму (это значение плюс 1) первому параметру.
Параметр timeout является указателем на структуру timeval, определяемую
следующим образом:
#include
struct timeval {
long tv_sec;
long tv_usec;
};

/* секунды */
/* микросекунды */

Если этот параметр не равен NULL, то вызов select() вернется через tv_sec секунд
и tv_usec микросекунд, даже если ни один из файловых дескрипторов не будет
готов к вводу-выводу. После возврата состояние этой структуры в различных
UNIX-подобных системах не определено, поэтому должно инициализироваться
заново (вместе с группами файловых дескрипторов) перед каждой активацией.
На самом деле современные версии Linux автоматически изменяют этот параметр,
устанавливая значения в оставшееся количество времени. Таким образом, если
величина задержки была установлена в 5 секунд и истекло 3 секунды с момента,
как файловый дескриптор перешел в состояниеготовности, tv.tv_sec после воз­
врата вызова будет иметь значение 2.
Если оба значения задержки равны нулю, то вызов вернется немедленно, сооб­
щив обо всех событиях, которые находились в режиме ожидания на момент вызо­
ва. Однако этот вызов не будет дожидаться никаких последующих событий.
Манипуляции с файловыми дескрипторами осуществляются не напрямую, а по­
средством вспомогательных макрокоманд. Благодаря этому системы UNIX могут
реализовывать группы дескрипторов так, как считается целесообразным. В боль­
шинстве систем, однако, эти группы реализованы как простые битовые массивы.
FD_ZERO удаляет все файловые дескрипторы из указанной группы. Эта команда
должна вызываться перед каждой активизацией select():
fd_set writefds;
FD_ZERO(&writefds);
1

Дело в том, что вызовы select() и poll() являются обрабатываемыми по уровню, а не по
фронту. Вызов epoll(), о котором мы поговорим в гл. 4, может работать в любом из этих
режимов. Операции, обрабатываемые по фронту, проще, но если пользоваться ими не­
аккуратно, то некоторые события могут быть пропущены.

85

Мультиплексный ввод-вывод

FD_SET добавляет файловый дескриптор в указанную группу, а FD_CLR удаляет
дескриптор из указанной группы:
FD_SET(fd, &writefds);
FD_CLR(fd, &writefds);

/* добавляем 'fd' к группе */
/* ой, удаляем 'fd' из группы */

В качественном коде практически не должно встречаться случаев, в которых
приходится воспользоваться FD_CLR, поэтому данная команда действительно ис­
пользуется очень редко.
FD_ISSET проверяет, принадлежит ли определенный файловый дескриптор к кон­
кретной группе. Если дескриптор относится к группе, то эта команда возвращает
ненулевое целое число, а если не относится, возвращает 0. FD_ISSET используется
после возврата вызова от select(). С его помощью мы проверяем, готов ли опреде­
ленный файловый дескриптор к действию:
if (FD_ISSET(fd, &readfds))
/* 'fd' доступен для неблокирующего считывания! */

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

Возвращаемые значения и коды ошибок
В случае успеха select() возвращает количество файловых дескрипторов, готовых
для ввода-вывода, во всех трех группах. Если была задана задержка, то возвраща­
емое значение может быть равно нулю. При ошибке вызов возвращает значение –1,
а errno устанавливается в одно из следующих значений:
 EBADF — в одной из трех групп оказался недопустимый файловый дескрип­

тор;
 EINVAL — сигнал был получен в период ожидания, и вызов можно повторить;
 ENOMEM — запрос не был выполнен, так как не был доступен достаточный объем

памяти.

Пример использования select()
Рассмотрим пример тривиальной, но полностью функциональной программы.
На нем вы увидите использование вызова select(). Эта программа блокируется,
дожидаясь поступления ввода на stdin, блокировка может продолжаться вплоть до
5 секунд. Эта программа отслеживает лишь один файловый дескриптор, поэтому
здесь отсутствует мультиплексный ввод-вывод как таковой. Однако данный пример
должен прояснить использование этого системного вызова:
#include
#include

86

Глава 2. Файловый ввод-вывод

#include
#include
#define TIMEOUT 5
#define BUF_LEN 1024

/* установка тайм-аута в секундах */
/* длина буфера считывания в байтах */

int main (void)
{
struct timeval tv;
fd_set readfds;
int ret;
/* Дожидаемся ввода на stdin. */
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
/* Ожидаем не дольше 5 секунд. */
tv.tv_sec = TIMEOUT;
tv.tv_usec = 0;
/* Хорошо, а теперь блокировка! */
ret = select (STDIN_FILENO + 1,
&readfds,
NULL,
NULL,
&tv);
if (ret == –1) {
perror ("select");
return 1;
} else if (!ret) {
printf ("%d seconds elapsed.\n", TIMEOUT);
return 0;
}
/*
* Готов ли наш файловый дескриптор к считыванию?
* (Должен быть готов, так как это был единственный fd,
* предоставленный нами, а вызов вернулся ненулевым,
* но мы же тут просто развлекаемся.)
*/
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buf[BUF_LEN+1];
int len;
/* блокировка гарантированно отсутствует */
len = read (STDIN_FILENO, buf, BUF_LEN);
if (len == –1) {
perror ("read");
return 1;
}
if (len) {

Мультиплексный ввод-вывод

}
}

}

87

buf[len] = '\0';
printf ("read: %s\n", buf);

return 0;

fprintf(stderr, "Этого быть не должно!\n");
return 1;

Использование select() для краткого засыпания
Исторически на различных UNIX-подобных системах вызов select() был более
распространен, чем механизм засыпания с разрешающей способностью менее се­
кунды, поэтому данный вызов часто используется как механизм для кратковремен­
ного засыпания. Чтобы использовать select() в таком качестве, достаточно указать
ненулевую задержку, но задать NULL для всех трех групп:
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
/* засыпаем на 500 микросекунд */
select (0, NULL, NULL, NULL, &tv);

В Linux предоставляются интерфейсы для засыпания с высоким разрешением.
О них мы подробно поговорим в гл. 11.

Вызов pselect()
Системный вызов select(), впервые появившийся в 4.2BSD, достаточно популярен,
но в POSIX есть и собственный вариант решения — вызов pselect(). Он был описан
сначала в POSIX 1003.1g-2000, а затем в POSIX 1003.1-2001:
#define _XOPEN_SOURCE 600
#include
int pselect (int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const struct timespec *timeout,
const sigset_t *sigmask);
/* эти же значения используются и сselect() */
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

Между системными вызовами pselect() и select() есть три различия.
1. Вызов pselect() использует для своего параметра timeout структуру timespec, а не
timeval. Структура timespec может иметь значения в секундах и наносекундах,

88

Глава 2. Файловый ввод-вывод

а не в секундах и микросекундах. Теоретически timespec должна создавать за­
держки с более высоким разрешением, но на практике ни одна из этих структур
не может надежно обеспечивать даже разрешение в микросекундах.
2. При вызове pselect() параметр timeout не изменяется. Следовательно, не требу­
ется заново инициализировать его при последующих вызовах.
3. Системный вызов select() не имеет параметра sigmask. Если при работе с сиг­
налами установить этот параметр в значение NULL, то pselect() станет функцио­
нально аналогичен select().
Структура timespec определяется следующим образом:
#include
struct timespec {
long tv_sec;
long tv_nsec;
};

/* секунды */
/* наносекунды */

Основная причина, по которой вызов pselect() был добавлен в инструментарий
UNIX, связана с появлением параметра sigmask. Этот параметр призван справлять­
ся с условиями гонки, которые могут возникать при ожидании файловых дескрип­
торов и сигналов. Подробнее о сигналах мы поговорим в гл. 10. Предположим, что
обработчик сигнала устанавливает глобальный флаг (большинство из них именно
так и делают), а процесс проверяет этот флаг перед вызовом select(). Далее пред­
положим, что сигнал приходит в период после проверки, но до вызова. Приложение
может оказаться заблокированным на неопределенный срок и так и не отреагиро­
вать на установленный флаг. Вызов pselect() позволяет решить эту проблему:
приложение может вызвать pselect(), предоставив набор сигналов для блокирова­
ния. Заблокированные сигналы не обрабатываются, пока не будут разблокированы.
Как только pselect() вернется, ядро восстановит старую маску сигнала.
До версии ядра Linux 2.6.16 pselect() был реализован в этой операционной
системе не как системный вызов, а как обычная обертка для вызова select(), пре­
доставляемая glibc. Такая обертка сводила к минимуму риск возникновения усло­
вий гонки, но не исключала его полностью. Когда pselect() стал системным вызо­
вом, проблема с условиями гонки была решена.
Несмотря на (относительно незначительные) улучшения, характерные для
pselect(), в большинстве приложений продолжает использоваться вызов select().
Это может делаться как по привычке, так и для обеспечения оптимальной перено­
симости.

Системный вызов poll()
Вызов poll() является в System V как раз тем решением, которое обеспечивает
мультиплексный ввод-вывод. Он компенсируют некоторые недостатки, имеющие­
ся у select(), хотя select() по-прежнему используется очень часто (как по привыч­
ке, так и для обеспечения оптимальной переносимости):

89

Мультиплексный ввод-вывод

#include
int poll (struct pollfd *fds, nfds_t nfds, int timeout);

В отличие от вызова select(), применяющего неэффективный метод с тремя
группами дескрипторов на основе битовых масок, poll() работает с единым масси­
вом структур nfds pollfd, на которые указывают файловые дескрипторы. Такая
структура определяется следующим образом:
#include
struct pollfd {
int fd;
short events;
short revents;
};

/* файловый дескриптор */
/* запрашиваемые события для отслеживания */
/* зафиксированные возвращаемые события */

В каждой структуре pollfd указывается один файловый дескриптор, который
будет отслеживаться. Можно передавать сразу несколько структур, указав poll()
отслеживать несколько файловых дескрипторов. Поле events каждой структуры
представляет собой битовую маску событий, которые мы собираемся отслеживать
на данном файловом дескрипторе. Ядро устанавливает это поле после возврата
значения. Все события, возвращенные в поле events, могут быть возвращены в поле
revents. Допустимы следующие события:
 POLLIN — имеются данные для считывания;
 POLLRDNORM — имеются обычные данные для считывания;
 POLLRDBAND — имеются приоритетные данные для считывания;
 POLLPRI — имеются срочные данные для считывания;
 POLLOUT — запись блокироваться не будет;
 POLLWRNORM — запись обычных данных блокироваться не будет;
 POLLWRBAND — запись приоритетных данных блокироваться не будет;
 POLLMSG — доступно сообщение SIGPOLL.

Кроме того, в поле revents могут быть возвращены следующие события:
 POLLER — возникла ошибка на указанном файловом дескрипторе;
 POLLHUP — событие зависания на указанном файловом дескрипторе;
 POLLNVAL — указанный файловый дескриптор не является допустимым.

Эти события не имеют никакого значения в поле events, и их не следует пере­
давать в данное поле, поскольку при необходимости система всегда их возвращает.
При использовании poll(), чего не скажешь о select(), не требуется явно задавать
необходимость отчета об исключениях.
Сочетание POLLIN | POLLPRI эквивалентно событию считывания в вызове
select(), а событие POLLOUT | POLLWRBAND идентично событию записи в вызове
select(). Значение POLLIN эквивалентно POLLRDNORM |POLLRDBAND, а POLLOUT эквива­
лентно POLLWRNORM.

90

Глава 2. Файловый ввод-вывод

Например, чтобы одновременно отслеживать на файловом дескрипторе воз­
можность считывания и возможность записи, следует задать для параметра events
значение POLLIN | POLLOUT. Получив возвращаемое значение, мы проверим поле
revents на наличие этих флагов в структуре, соответствующей интересующему нас
файловому дескриптору. Если бы флаг POLLOUT был установлен, то файловый дес­
криптор был бы доступен для записи без блокирования. Эти флаги не являются
взаимоисключающими: можно установить сразу оба, обозначив таким образом, что
возможен возврат и при считывании, и при записи. Блокирования при считывании
и записи на этом файловом дескрипторе не будет.
Параметр timeout указывает задержку (длительность ожидания) в миллисекундах
перед возвратом независимо от наличия или отсутствия готового ввода-вывода. От­
рицательное значение соответствует неопределенно долгой задержке. Значение 0
предписывает вызову вернуться незамедлительно, перечислив все файловые дес­
крипторы, на которых имеется ожидающий обработки ввод-вывод, но не дожидаясь
каких-либо дальнейших событий. Таким образом, poll() оправдывает свое название:
он совершает один акт опроса и немедленно возвращается.

Возвращаемые значения и коды ошибок
В случае успеха poll() возвращает количество файловых дескрипторов, чьи струк­
туры содержат ненулевые поля revents. В случае возникновения задержки до какихлибо событий этот вызов возвращает 0. При ошибке возвращается –1, а errno уста­
навливается в одно из следующих значений:
 EBADF — для одной или нескольких структур были заданы недопустимые фай­
ловые дескрипторы;
 EFAULT — значение указателя на файловые дескрипторы находится за пределами
адресного пространства процесса;
 EINTR — до того как произошло какое-либо из запрошенных событий, был выдан
сигнал; вызов можно повторить;
 EINVAL — параметр nfds превысил значение RLIMIT_NOFILE;
 ENOMEM — для выполнения запроса оказалось недостаточно памяти.

Пример использования poll()
Рассмотрим пример программы, использующей вызов poll()для одновременной
проверки двух условий: не возникнет ли блокировка при считывании с stdin и за­
писи в stdout:
#include
#include
#include
#define TIMEOUT 5 /* задержка poll, значение в секундах */
int main (void)
{
struct pollfd fds[2];

Мультиплексный ввод-вывод

91

int ret;
/* отслеживаем ввод на stdin */
fds[0].fd = STDIN_FILENO;
fds[0].events= POLLIN;
/* отслеживаем возможность записи на stdout (практически всегда true) */
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;
/* Все установлено, блокируем! */
ret= poll(fds, 2, TIMEOUT* 1000);
if (ret == –1) {
perror ("poll");
return 1;
}
if (!ret) {
printf ("%d seconds elapsed.\n", TIMEOUT);
return 0;
}
if (fds[0].revents &POLLIN)
printf ("stdin is readable\n");
if (fds[1].revents &POLLOUT)
printf ("stdout is writable\n");
return 0;
}

Запустив этот код, мы получим следующий результат, как и ожидалось:
$ ./poll
stdout is writable

Запустим его еще раз, но теперь перенаправим файл в стандартный ввод, и мы
увидим оба события:
$ ./poll0 &&(c = fgetc (stream)) != EOF)
*s++ = c;
*s = '\0';

Этот пример можно изменить, чтобы не считывался и разделитель (в качестве
разделителя выступает целое число d, в этом примере мы не можем использовать
в таком качестве символ нуля).
char *s;
int c = 0;
s = str;
while (--n >0 &&(c = fgetc (stream)) != EOF &&(*s++ = c) != d)
;
if (c == d)
*--s = '\0';
else
*s= '\0';

Если установить значение d в \n, получим примерно такое же поведение, как
и у fgets(), но уже не будем сохранять в буфере переход на новую строку.
В зависимости от реализации fgets() этот вариант может работать медленнее,
так как он неоднократно делает вызовы функции fgetc(). Однако это немного иная
проблема, чем показанная в нашем исходном примере с dd! Да, в последнем фраг­
менте мы имеем издержки, связанные с дополнительными вызовами функций, но
избавляемся от нагрузки, которая была обусловлена избыточными системными
вызовами, а также от неудобств, связанных с невыровненным вводом-выводом при
dd с bs=1, — последняя проблема намного серьезнее.

Считывание из потока данных

109

Считывание двоичных данных
В некоторых приложениях считывать отдельные символы или строки недостаточ­
но. Иногда разработчику требуется считывать и записывать сложные двоичные
данные, в частности структуры C. Для этой цели в стандартной библиотеке вводавывода предоставляется функция fread():
#include
size_t fread (void *buf, size_t size, size_t nr, FILE *stream);

При вызове fread() мы можем прочитать вплоть до nr фрагментов данных, каж­
дый размером size байт. Считывание происходит из потока данных stream в буфер,
указанный buf. Значение файлового указателя увеличивается на число, равное
количеству прочитанных байтов.
Возвращается количество считанных элементов (но не количество считанных
байтов!). При ошибке или достижении конца файла функция возвращает значение,
меньшее чем nr. К сожалению, мы не можем узнать, какое именно из двух условий
наступило, — для этого потребуется специально применить функции ferror() и feof()
(см. разд. «Ошибки и конец файла» этой главы).
Из-за различий, связанных с переменным размером, выравниванием, заполне­
нием и порядком байтов, двоичные данные, записанные одним приложением, могут
быть непригодны для считывания другим приложением либо даже тем же самым
приложением на другой машине.
Проблемы, связанные с выравниванием
Во всех машинных архитектурах действуют свои требования к выравниванию данных. Обычно программисты представляют себе память просто как массив буферов.
Однако процессор читает и записывает память не фрагментами, равными энному
количеству байтов. При обращении к памяти процессор делает это с конкретной
детализацией — например, по 2, 4, 8 или 16 байт. Адресное пространство каждого
процесса начинается с 0, поэтому процессы должны инициировать доступ с адреса, который является целочисленным кратным единицы детализации.
Соответственно, сохранять переменные C и обращаться к ним нужно с выровненных адресов. В принципе, переменным свойственно естественное выравнивание,
то есть выравнивание, соответствующее размеру типа данных в языке C. Например,
32-битное целое число выровнено по 4-байтовым границам. Иными словами, в большинстве архитектур int должно сохраняться в памяти по адресу, значение которого
без остатка делится на 4.
При доступе к невыровненным данным возникают различные проблемы, зависящие от машинной архитектуры. Некоторые процессоры могут обращаться к невыровненным данным, но за счет снижения производительности. Другие процессоры вообще не умеют обращаться к невыровненным данным, при попытке это
сделать возникает аппаратное исключение. Хуже того, некоторые процессоры просто бесшумно удаляют младшие биты, чтобы принудительно добиться выравнивания
адреса. Практически всегда это приводит к непредвиденным последствиям.

110

Глава 3. Буферизованный ввод-вывод

Как правило, компилятор естественным образом выравнивает все данные, и это
не представляет заметной проблемы для программиста. Однако при работе со
структурами, управлении памятью вручную, сохранении двоичных данных на диск
и обмене информацией по сети вопросы выравнивания данных могут стать актуальными, поэтому системный программист должен уметь справляться с подобными сложностями.
Мы подробнее поговорим о выравнивании в гл. 9.
Простейший образец fread() применяется для считывания одного элемента
линейных байтов из указанного потока данных:
char buf[64];
size_t nr;
nr = fread (buf, sizeof(buf), 1, stream);
if(nr== 0)
/* ошибка */

Ниже мы рассмотрим более сложные примеры — они встретятся вам при изу­
чении записывающей функции fwrite(), парной для fread().

Запись в поток данных
Стандартная библиотека C содержит функции не только для чтения, но и для за­
писи в открытый поток данных. В этом разделе мы рассмотрим три наиболее по­
пулярных метода записи: запись отдельного символа, запись строки символов
и запись двоичных данных. Такие разнообразные варианты записи идеально под­
ходят для буферизованного ввода-вывода. Чтобы мы могли записать информацию
в поток данных, он должен быть открыт как поток данных вывода в нужном режи­
ме. Подойдут любые допустимые режимы, кроме r.

Запись отдельного символа
Функция, парная fgetc(), называется fputc():
#include
int fputc (int c, FILE *stream);

Функция fputc() записывает байт, указанный в c (приведенный к unsigned char),
в поток данных, указанный в stream. При успешном завершении операции функция
возвращает c. В противном случае она возвращает EOF и присваивает errno со­
ответствующее значение.
Пример использования прост:
if(fputc('p', stream) == EOF)
/* ошибка */

Запись в поток данных

111

В данном примере мы записываем символ p в поток данных stream, который
должен быть открыт для записи.

Запись строки символов
Функция fputs() используется для записи целой строки в заданный поток данных:
#include
int fputs (const char *str, FILE *stream);

При вызове fputs() все содержимое строки, оканчивающейся нулем, записывается
в поток данных, указанный в stream. Сама эта строка указывается в str. В случае
успеха fputs() возвращает неотрицательное число. При ошибке она возвращает EOF.
В следующем примере файл открывается для внесения данных в режиме доза­
писи. Указанная строка записывается в ассоциированный поток данных, после чего
этот поток данных закрывается:
FILE *stream;
stream = fopen ("journal.txt", "a");
if (!stream)
/* ошибка */
if (fputs ("The ship is made of wood.\n", stream) == EOF)
/* ошибка */
if (fclose (stream) == EOF)
/* ошибка */

Запись двоичных данных
Отдельных символов и строк недостаточно, если программе требуется записывать
сложные данные. Для непосредственного сохранения двоичных данных, например
переменных языка C, в рамках стандартного ввода-вывода предоставляется функ­
ция fwrite():
#include
size_t fwrite (void *buf,
size_t size,
size_t nr,
FILE *stream);

При вызове fwrite() в поток данных stream записывается вплоть до nr элементов,
каждый до size в длину. Берутся данные из буфера, указанного в buf. Значение
файлового указателя увеличивается на число, равное общему количеству записан­
ных байтов.
Возвращается количество успешно записанных элементов (но не количество
байтов!). Возвращаемое значение меньше nr означает ошибку.

112

Глава 3. Буферизованный ввод-вывод

Пример программы, в которой используется
буферизованный ввод-вывод
Давайте рассмотрим пример — фактически полнофункциональную программу,
включающую в себя многие интерфейсы, уже изученные нами в этой главе.
Сначала программа определяет структуру struct pirate, а потом определяет две
переменные такого типа. Программа инициализирует одну из переменных, а по­
том записывает ее на диск через поток данных вывода в файл data. В другом по­
токе данных программа вновь считывает данные из файла data непосредственно
в другой экземпляр struct pirate. Наконец, программа записывает содержимое
структуры в стандартный вывод:
#include
int main (void)
{
FILE *in, *out;
struct pirate {
char
name[100];
/* настоящее имя */
unsigned long booty;
/* вознаграждение в фунтах стерлингов */
unsigned int beard_len;
/* длина бороды в дюймах */
} p, blackbeard= { "EdwardTeach", 950, 48 };
out = fopen ("data", "w");
if (!out) {
perror ("fopen");
return 1;
}
if (!fwrite (&blackbeard, sizeof (struct pirate), 1, out)) {
perror ("fwrite");
return 1;
}
if (fclose (out)) {
perror ("fclose");
return 1;
}
in = fopen ("data", "r");
if (!in) {
perror ("fopen");
return 1;
}
if (!fread (&p, sizeof (struct pirate), 1, in)) {
perror ("fread");
return 1;

Позиционирование в потоке данных

113

}
if (fclose (in)) {
perror ("fclose");
return 1;
}
printf ("name=\"%s\" booty=%lu beard_len=%u\n",
p.name, p.booty, p.beard_len);
return 0;
}

На выходе, разумеется, имеем исходные значения:
name="Edward Teach" booty=950 beard_len=48

Опять же важно помнить, что из-за различий размеров переменных, при вырав­
нивании и т. д. двоичные данные, записанные в одном приложении, могут оказать­
ся нечитаемыми в других. Это означает, что иное приложение — или даже то же
самое приложение на другой машине — может оказаться не в состоянии правильно
считать данные, записанные с помощью fwrite(). В нашем примере представьте
себе варианты развития событий, если изменится размер unsigned long или степень
заполнения. Эти параметры гарантированно остаются постоянными лишь на ма­
шине определенного типа с конкретным интерфейсом ABI.

Позиционирование в потоке данных
Часто бывает полезно манипулировать текущей позицией в потоке данных. До­
пустим, приложение считывает сложный файл, требующий манипулирования
записями, и программа должна произвольно перемещаться по этому файлу. В дру­
гой ситуации, возможно, придется сбрасывать поток данных на нулевую позицию.
Для подобных случаев в стандартной библиотеке ввода-вывода предоставляется
семейство интерфейсов, функционально эквивалентных системному вызову
lseek() (он был рассмотрен в гл. 2). Функция fseek(), наиболее распространенный
интерфейс позиционирования из инструментов стандартного ввода-вывода,
управляет файловой позицией в потоке данных stream в зависимости от значений
offset и whence:
#include
int fseek (FILE *stream, long offset, int whence);

Если аргумент whence имеет значение SEEK_SET, то файловая позиция устанав­
ливается в offset. Если whence равен SEEK_CUR, файловая позиция получает значе­
ние, равное «текущая позиция плюс offset». Если whence имеет значение SEEK_END,
то файловая позиция устанавливается в значение, равное «конец файла плюс
offset».

114

Глава 3. Буферизованный ввод-вывод

При успешном завершении функция fseek() возвращает 0, стирает индикатор
EOF и отменяет любые эффекты функции ungetc() (при их наличии). При ошибке
она возвращает –1 и устанавливает errno в соответствующее значение. Самые рас­
пространенные ошибки — это недействительный поток данных (EBADF) и недей­
ствительный аргумент whence (EINVAL).
В качестве альтернативы для стандартного ввода-вывода предоставляется функ­
ция fsetpos():
#include
int fsetpos(FILE*stream, fpos_t*pos);

Эта функция устанавливает позицию в потоке данных stream в значение pos.
Она действует так же, как и функция fseek() с аргументом whence, равным SEEK_SET.
В случае успеха эта функция возвращает 0. В противном случае она возвращает –1
и присваивает errno соответствующее значение. Эта функция (а также парная
ей функция fgetpos(), которую мы вскоре рассмотрим) предоставлена исключи­
тельно для работы с не относящимися к UNIX платформами, где для представления
позиции в потоке данных используются сложные типы. При взаимодействии с та­
кими платформами данная функция — единственный инструмент, позволяющий
установить произвольную позицию в потоке данных, поскольку возможностей типа
long языка C недостаточно. В специфичных для Linux приложениях этот интерфейс
использовать нет необходимости, хотя и можно, если требуется обеспечить макси­
мальную межплатформенную совместимость.
В стандартной библиотеке ввода-вывода также предоставляется функция бы­
строго доступа rewind():
#include
void rewind (FILE *stream);

Вызов этой функции:
rewind (stream);

сбрасывает значение позиции обратно на начало потока данных. Код эквивалентен
следующему:
fseek (stream, 0, SEEK_SET);

с оговоркой, что она также очищает индикатор ошибки.
Обратите внимание: функция rewind() не имеет возвращаемого значения и по­
этому не может напрямую сообщать о возникающих ошибках. Если вызывающей
стороне требуется убедиться в наличии ошибки, то понадобится очистить errno до
вызова, а потом проверить, сохранила ли переменная после этого ненулевое значе­
ние. Например:
errno= 0;
rewind(stream);
if(errno)
/* ошибка */

Сброс потока данных

115

Получение актуальной позиции в потоке данных. В отличие от lseek(), fseek()
не возвращает обновленную позицию. Для этого предоставляется отдельный
интерфейс. Функция ftell() возвращает текущую позицию из потока данных
stream:
#include
long ftell (FILE *stream);

При ошибке она возвращает –1 и устанавливает errno в соответствующее значе­
ние.
В рамках стандартного ввода-вывода предоставляется и альтернативная функ­
ция fgetpos():
#include
int fgetpos(FILE*stream, fpos_t*pos);

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

Сброс потока данных
В стандартной библиотеке ввода-вывода есть интерфейс, позволяющий выписать
содержимое пользовательского буфера в ядро. Он гарантирует, что все данные,
записанные в поток данных, будут сброшены к ядру с помощью write(). Функция
fflush() действует следующим образом:
#include
int fflush (FILE *stream);

При вызове этой функции все незаписанные данные из потока данных, указан­
ного в stream, сбрасываются в буфер ядра. Если значение stream равно NULL, то все
открытые потоки данных этого процесса записываются в буфер ядра. В случае
успеха fflush() возвращает 0. В случае ошибки эта функция возвращает EOF и при­
сваивает errno соответствующее значение.
Чтобы понять принцип действия fflush(), следует понимать разницу между
буфером, поддерживаемым библиотекой C, и буферизацией, выполняемой в самом
ядре. Все вызовы, описанные в данной главе, работают с буфером, поддерживаемым
библиотекой C. Этот буфер располагается в пользовательском пространстве, и, сле­
довательно, в нем работает пользовательский код, а не выполняются системные
вызовы. Системный вызов выдается, только когда необходимо обратиться к диску
или какому-нибудь другому носителю.

116

Глава 3. Буферизованный ввод-вывод

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

Ошибки и конец файла
Некоторые интерфейсы стандартного ввода-вывода, например fread(), плохо справ­
ляются с информированием вызывающей стороны о сбоях, так как в них отсут­
ствует механизм, позволяющий отличать ошибку от конца файла. При работе
с такими вызовами, а также в некоторых других случаях бывает полезно проверить
статус конкретного потока данных и определить, установлен ли на потоке данных
stream индикатор ошибки:
#include
int ferror(FILE*stream);

Индикатор ошибки устанавливается другими интерфейсами стандартного вво­
да-вывода в ответ на возникновение условия ошибки. Функция возвращает нену­
левое значение, если индикатор установлен, а в других случаях возвращает 0.
Функция feof() проверяет, установлен ли на потоке данных stream индикатор
EOF:
#include
int feof(FILE*stream);

Индикатор EOF устанавливается другими интерфейсами стандартного вводавывода, когда достигается конец файла. Функция возвращает ненулевое значение,
если индикатор установлен, а в других случаях возвращает 0.
Функция clearerr() удаляет с потока данных stream индикаторы ошибки и EOF:
#include
void clearerr(FILE*stream);

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

Управление буферизацией

117

/* 'f' — допустимый поток данных */
if(ferror(f))
printf("Erroronf!\n");
if (feof (f))
printf ("EOF on f!\n");
clearerr (f);

Получение ассоциированного файлового
дескриптора
Иногда предпочтительно получить файловый дескриптор, лежащий в основе кон­
кретного потока данных. Например, нам может потребоваться выполнить приме­
нительно к потоку данных системный вызов. Это будет выполняться через файло­
вый дескриптор потока данных, когда ассоциированной функции стандартного
ввода-вывода не существует. Чтобы получить файловый дескриптор, лежащий
в основе потока данных, используйте fileno():
#include
int fileno(FILE*stream);

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

Управление буферизацией
При стандартном вводе-выводе реализуется три типа пользовательской буфериза­
ции, и она предоставляет разработчикам интерфейс, позволяющий контролировать
тип и размер буфера. Различные виды пользовательской буферизации применя­
ются для разных целей и идеально подходят каждый для своей ситуации.
 Без буферизации. Пользовательская буферизация не применяется. Данные

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

118

Глава 3. Буферизованный ввод-вывод

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

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

случае блок — это фиксированное количество байтов. Именно о таком типе
буферизации мы говорили в начале этой главы, такая буферизация идеально
подходит для работы с файлами. По умолчанию поблочная буферизация при­
меняется со всеми потоками данных, которые ассоциированы с файлами. В кон­
тексте стандартного ввода-вывода поблочная буферизация называется полной
буферизацией.
В большинстве случаев заданный по умолчанию тип буферизации является
оптимальным. Тем не менее стандартный ввод-вывод предоставляет интерфейс
для управления типом применяемой буферизации:
#include
int setvbuf (FILE *stream, char *buf, int mode, size_t size);

Функция setvbuf() устанавливает тип буферизации для потока данных stream
в значение mode, которое может быть одним из следующих:
 _IONBF — без буферизации;
 _IOLBF — построчная буферизация;
 _IOFBF — поблочная буферизация.

За исключением значения _IONBF, при котором buf и size игнорируются, buf
может указывать на буфер размером size байт. Стандартный ввод-вывод будет
использовать этот буфер для данных указанного потока данных. Если buf равен
NULL, то буфер указанного вами размера автоматически выделяется библиотекой
glibc.
Функция setvbuf() может быть вызвана после открытия потока данных, но до
того, как с ним будут выполняться какие-либо иные манипуляции. При успехе она
возвращает 0, в другом случае — ненулевое значение.
Если буфер предоставлен, то он должен существовать и после закрытия потока
данных. Распространенная ошибка — когда буфер определяется как автоматическая
переменная в области видимости, заканчивающейся до закрытия потока данных.
В частности, не следует создавать буфер, локальный для main(), так как впослед­
ствии вы не сможете явно закрывать потоки данных. Например, следующий код
содержит ошибку:

Безопасность программных потоков

119

#include
int main (void)
{
charbuf[BUFSIZ];
/* задаем для stdin поблочную буферизацию с буфером BUFSIZ */
setvbuf(stdout, buf, _IOFBF, BUFSIZ);
printf ("Arrr!\n");
return 0;
/* 'buf' выходит из области видимости и высвобождается, но stdout закрывается
уже позже */
}

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

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

120

Глава 3. Буферизованный ввод-вывод

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

Блокировка файлов вручную
Функция flockfile() дожидается, пока не будет снята блокировка с потока данных
stream, увеличивает количество блокировок на единицу, а потом получает блоки­
ровку и становится владеющим программным потоком этого потока данных. Затем
она возвращается:
#include
void flockfile (FILE *stream);
1

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

Безопасность программных потоков

121

Функция funlockfile() уменьшает количество блокировок, ассоциированное
с потоком данных stream:
#include
void funlockfile (FILE*stream);

Если количество блокировок достигает нуля, то текущий программный поток
прекращает владеть соответствующим потоком данных. Теперь блокировку сможет
получить другой программный поток.
Такие вызовы могут быть вложенными. Это означает, что отдельно взятый
программный поток может выдавать множественные вызовы flockfile() и поток
данных не разблокируется, пока процесс не выдаст ровно такое же количество
вызовов funlockfile().
Функция ftrylockfile() представляет собой неблокирующую разновидность
flockfile():
#include
int ftrylockfile (FILE*stream);

Если поток данных stream в конкретный момент заблокирован, то ftrylockfile()
ничего не делает и сразу же возвращает ненулевое значение. Если поток данных
stream в этот момент не заблокирован, функция получает блокировку, увеличивает
количество блокировок, становится владеющим программным потоком потока
данных stream и возвращает 0.
Рассмотрим пример. Допустим, мы хотим записать несколько строк в файл и га­
рантировать, что при записи они не будут перемежаться с операциями записи,
идущими от других потоков:
flockfile (stream);
fputs ("List of treasure:\n", stream);
fputs (" (1) 500 gold coins\n", stream);
fputs (" (2) Wonderfully ornate dishware\n", stream);
funlockfile (stream);

Хотя отдельные операции fputs() никогда не вступают в условия гонки с други­
ми операциями ввода/вывода — например, у вас никогда не возникнет пересечения
чего бы то ни было с List of treasure, — в работу такой функции может вклиниться
другая операция стандартного ввода-вывода, поступающая от другого программно­
го потока к тому же потоку данных. Это может происходить в промежутке между
двумя вызовами fputs(). В идеальном случае приложение должно быть построено
так, чтобы множественные программные потоки не направляли ввод-вывод в один
и тот же поток данных. Если же в вашем приложении такая отправка необходима,
но при этом нужен и атомарный регион шире одной функции, то flockfile() со то­
варищи вам очень пригодятся.

122

Глава 3. Буферизованный ввод-вывод

Неблокируемые потоковые операции
Есть и другая причина, по которой может потребоваться блокировать потоки
данных вручную. Обеспечив более тонкий и точный контроль блокировок (это
под силу только системному программисту), высможете минимизировать издерж­
ки, связанные с блокировками, и улучшить таким образом производительность.
Для этой цели в Linux предоставляется семейство функций, родственных стан­
дартным интерфейсам ввода-вывода. Функции из этого семейства отличаются
тем, что вообще не выполняют блокировок. Фактически это неблокируемые ана­
логи функций стандартного ввода-вывода:
#define _GNU_SOURCE
#include
int fgetc_unlocked (FILE *stream);
char *fgets_unlocked (char *str, int size, FILE *stream);
size_t fread_unlocked (void *buf, size_t size, size_t nr,
FILE *stream);
int fputc_unlocked (int c, FILE *stream);
int fputs_unlocked (const char *str, FILE *stream);
size_t fwrite_unlocked (void *buf, size_t size, size_t nr,
FILE *stream);
int fflush_unlocked (FILE *stream);
int feof_unlocked (FILE *stream);
int ferror_unlocked (FILE *stream);
int fileno_unlocked (FILE *stream);
void clearerr_unlocked (FILE *stream);

Все эти функции работают идентично своим блокируемым аналогам, но не
проверяют наличия блокировок и не получают блокировку, ассоциированную
с конкретным потоком stream. Если блокировка необходима, то именно програм­
мист должен вручную обеспечить ее получение и последующее высвобожде­
ние.
ВНИМАНИЕ
При использовании неблокируемых стандартных функций ввода-вывода значительно повышается производительность. Более того, код радикально упрощается, если не приходится беспокоиться о блокировании сложных операций с помощью flockfile(). Создавая приложение, попробуйте передать весь ввод-вывод в один программный поток (либо перепоручить его пулу
программных потоков, в котором каждый поток данных отображается ровно на один програм­
мный поток).

Хотя в POSIX и определяется несколько неблокируемых вариантов функций
стандартного ввода-вывода, ни одна из вышеприведенных функций в POSIX не
описана. Все они специфичны для Linux, хотя это подмножество и поддерживает­
ся в некоторых других системах UNIX.
Мы подробно поговорим о программных потоках в гл. 7.

Резюме

123

Недостатки стандартного ввода-вывода
Как ни часто используется стандартный ввод-вывод, часть экспертов указывают
на определенные его недочеты. Некоторые функции, например fgets(), бывают
неэффективными или плохо сработанными. Другие функции, к примеру gets(),
настолько ужасны, что их практически изгнали из всех стандартов.
Самые серьезные претензии к стандартному вводу-выводу связаны с его нега­
тивным влиянием на производительность, которое объясняется необходимостью
двойного копирования. При считывании данных стандартный ввод-вывод направ­
ляет к ядру системный вызов read(), копируя данные из ядра в буфер стандартно­
го ввода-вывода. Если после этого приложение с помощью стандартного вводавывода делает запрос на запись, применяя, скажем, fgetc(), то данные копируются
вновь — на этот раз из буфера стандартного ввода-вывода в указанный буфер. За­
просы на запись работают прямо противоположным образом: сначала данные ко­
пируются из предоставленного буфера в буфер стандартного ввода/вывода, а от­
туда — в ядро посредством вызова write().
Альтернативная реализация позволяет избежать двойного копирования. Для это­
го нужно при каждом запросе на считывание возвращать указатель на буфер стан­
дартного ввода-вывода. После этого данные можно считывать напрямую внутри
буфера стандартного ввода-вывода без необходимости дополнительного копирова­
ния. Если приложению действительно потребуются данные из его собственного
локального буфера — например, чтобы что-то дописать к ним, — то мы всегда можем
организовать копирование вручную. Такая реализация предоставляла бы «свобод­
ный» интерфейс, позволяющий приложениям сигнализировать о завершении об­
работки конкретного фрагмента кода из буфера считывания.
Запись протекала бы несколько сложнее, но двойного копирования по-преж­
нему не было бы. При выдаче запроса на запись эта реализация требовала бы ис­
пользовать указатель. В конечном итоге, когда данные были бы готовы к сбросу
в ядро, такая реализация могла пройти по всему списку сохраненных указателей,
записывая данные. Это можно сделать посредством фрагментирующего/дефраг­
ментирующего ввода-вывода, для которого требуется лишь один системный вызов
writev(). Мы поговорим о фрагментирующем/дефрагментирующем вводе-выводе
в следующей главе.
Есть исключительно удобные пользовательские библиотеки, решающие про­
блему двойного копирования посредством реализаций, очень напоминающих
предложенную выше. В качестве альтернативы многие разработчики реализуют
собственные решения, обеспечивающие буферизацию. Однако, несмотря на аль­
тернативы, стандартный ввод-вывод остается популярным.

Резюме
Стандартный ввод-вывод — это библиотека для пользовательской буферизации,
предоставляемая в рамках стандартной библиотеки C. Несмотря на определенные

124

Глава 3. Буферизованный ввод-вывод

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

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

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

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

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

4

Расширенный
файловый ввод-вывод

В гл. 2 мы рассмотрели простейшие системные вызовы ввода-вывода, применяемые
в Linux. Эти вызовы являются не только базисом файлового ввода-вывода, но и ос­
новой практически любого обмена информацией в Linux. В гл. 3 мы изучали, зачем
может потребоваться пользовательская буферизация, зачастую выстраиваемая на
базе простейших системных вызовов ввода-вывода. Мы также ознакомились с ре­
шением, специально предназначенным для выполнения пользовательской буфе­
ризации, — стандартной библиотекой С. В этой главе мы займемся расширенными
системными вызовами ввода-вывода, имеющимися в Linux.
 Фрагментированный ввод-вывод. Позволяет отдельно взятому вызову запи­

сывать данные или считывать их из многих буферов одновременно. Полезен
для объединения в пакеты полей из разных структур данных и для формирова­
ния транзакции ввода-вывода.
 Интерфейс epoll. Усовершенствованный вариант системных вызовов poll()
и select(), описанных в гл. 2. Полезен, когда в ходе опроса требуется получить

из одного потока сотни файловых дескрипторов.
 Ввод-вывод с отображением в память. Отображает файл на пространство па­

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

как предполагается использовать файл в данном процессе; может улучшить
производительность ввода-вывода.
 Асинхронный ввод-вывод. Позволяет процессу выдавать запросы на ввод-вы­

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

Фрагментированный ввод-вывод
Фрагментированный ввод-вывод — это способ ввода-вывода, при котором отдельно
взятый системный вызов позволяет записать вектор буферов из одного потока

126

Глава 4. Расширенный файловый ввод-вывод

данных. Данный тип ввода-вывода получил такое название, поскольку данные
в виде небольших фрагментов рассыпаны в векторе буферов, откуда программе
приходится их собирать. Альтернативное название этого подхода — векторный
ввод/вывод. Стандартные системные вызовы, применяемые для чтения и записи,
рассмотренные нами в гл. 2, обеспечивают линейный ввод/вывод.
Фрагментированный ввод/вывод имеет преимущества перед линейным.
 Более логичный принцип написания кода. Если ваши данные сегментированы
естественным образом — допустим, представлены в виде полей, имеющих зара­
нее определенную структуру, — векторный ввод-вывод обеспечивает интуитив­
но понятные манипуляции с кодом.
 Эффективность. Всего одна операция векторного ввода-вывода может заменить
множество операций линейного.
 Производительность. Кроме снижения количества выполняемых системных
вызовов, векторная реализация ввода-вывода может обеспечить более высокую
производительность по сравнению с линейной, что достигается посредством
внутрисистемной оптимизации.
 Атомарность. В отличие от ситуации с множественными линейными операциями
ввода-вывода, процесс может выполнять всего одну операцию векторного вводавывода без риска наложения на ввод-вывод, происходящий в другом процессе.

Системные вызовы readv() и writev()
Стандарт POSIX 1003.1-2001 описывает, а Linux реализует пару системных вызо­
вов, обеспечивающих фрагментированный ввод-вывод. Реализация Linux соответ­
ствует всем преимуществам, перечисленным в предыдущем разделе.
Функция readv() считывает count сегментов из файлового дескриптора fd в бу­
феры, описанные iov:
#include
ssize_t readv (int fd,
const struct iovec *iov,
int count);

Функция writev()записывает не более count сегментов из буферов, описанных
в iov, в файловый дескриптор fd:
#include
ssize_t writev (int fd,
const struct iovec *iov,
int count);

Функции readv() и writev() работают так же, как и read() и write() соответ­
ственно, за тем исключением, что считывание или запись в первых двух случаях
затрагивает сразу множество буферов.
Каждая структура iovec описывает самостоятельный отдельный буфер, назы­
ваемый сегментом:

Системные вызовы readv() и writev()

127

#include
struct iovec{
void *iov_base; /* указатель на начало буфера */
size_t iov_len; /* размер буфера в байтах */
};

Набор сегментов называется вектором. Каждый сегмент в векторе описывает
адрес и длину того буфера в памяти, в который должны быть записаны данные (или
из которого они должны быть считаны). Функция readv() целиком заполняет каж­
дый буфер, имеющий длину iov_len байт, после чего переходит к следующему
буферу. Функция writev() всегда записывает полный буфер iov_len, прежде чем
перейти к следующему буферу. Обе функции всегда обрабатывают сегменты по
порядку, начиная с iov[0], затем iov[0] и т. д. до iov[count-1].

Возвращаемые значения
В случае успеха функции readv() и writev() возвращают количество считанных или
записанных байт соответственно. Это количество должно быть суммой всех значе­
ний count iov_len. При ошибке эти вызовы возвращают –1 и устанавливают errno
в соответствующее значение. Эти системные вызовы могут сталкиваться с любыми
из ошибок, которые характерны для read() и write(), и в случае получения таких
ошибок будут выдавать те же коды errno. Кроме того, стандарты определяют для
readv() и writev() еще два варианта ошибки.
Во-первых, поскольку возвращаемый тип равен ssize_t, то, если сумма всех
значений iov_len превышает SSIZE_MAX, никакие данные не будут возвращены, а про­
сто будет возвращено значение –1, а errno будет установлено в EINVAL.
Во-вторых, POSIX требует, чтобы count было больше нуля, но меньше или рав­
но IOV_MAX, что определяется в . В современных системах Linux значение
IOV_MAX составляет 1024. Если count равно нулю, то эти системные вызовы возвра­
щают 01. Если значение count превышает IOV_MAX, то никакие данные не передаются,
вызовы возвращают –1, а errno устанавливается в значение EINVAL.
ПРИМЕЧАНИЕ
В ходе операций векторного ввода-вывода ядро Linux должно выделять внутренние структуры данных
для представления каждого сегмента. Как правило, такое выделение происходит динамически, в зависимости от значения count. Однако в целях оптимизации ядро Linux создает в стеке небольшой массив
сегментов, которым пользуется, когда значение count достаточно невелико. В результате исчезает
необходимость динамического выделения сегментов и производительность несколько улучшается. В настоящее время данный порог равен 8, поэтому если значение count меньше или равно 8, то векторные
операции ввода-вывода очень экономно расходуют память и происходят за рамками стека ядра.
Скорее всего, вы не сможете выбрать, сколько сегментов информации придется единовременно
передавать в рамках одной векторной операции ввода-вывода. Однако если вы работаете гибко
и всерьез раздумываете над такими мелкими значениями, то при выборе значения 8 и менее производительность можно существенно увеличить.

1

Обратите внимание: в других системах UNIX errno может устанавливаться в EINVAL,
если count равно нулю. Это явно разрешено в стандартах, указывающих, что при нуле­
вом значении можно устанавливать EINVAL, если значение действительно равно нулю,
и система может обработать такой нулевой случай иным (неошибочным) способом.

128

Глава 4. Расширенный файловый ввод-вывод

Пример использования writev()
Рассмотрим простой пример, в котором записывается вектор из трех сегментов,
причем все три содержат по строке разного размера. Эта самодостаточная програм­
ма вполне детально описывает writev(), оставаясь при этом компактной:
#include
#include
#include
#include
#include
#include








int main ()
{
struct iovec iov[3];
ssize_t nr;
int fd, i;
char *buf[] = {
"The term buccaneer comes from the word boucan.\n",
"A boucan is a wooden frame used for cooking meat.\n",
"Buccaneer is the West Indies name for a pirate.\n" };
fd = open ("buccaneer.txt", O_WRONLY | O_CREAT | O_TRUNC);
if (fd == –1) {
perror ("open");
return 1;
}
/* заполняем три структуры iovec */
for(i= 0; iwd, event->mask,
event->cookie, event->len,
(event->mask & IN_ISDIR) ? "yes" : "no");
/* при наличии имени выводим его на экран */
if (event->len)
printf ("name=%s\n", event->name);
/* обновляем индекс, чтобы он указывал на начало следующего события*/
i += sizeof (struct inotify_event) + event->len;
}

Файловый дескриптор inotify действует как обычный файл, поэтому програм­
мы могут наблюдать за ним с помощью вызовов select(), poll() и epoll(). Таким
образом, процессы могут мультиплексировать события inotify с другим файловым
вводом-выводом от конкретного потока.

Расширенные события inotify
Кроме стандартных событий, inotify может генерировать и другие события.
 IN_IGNORED — страж, соответствующий дескриптору wd, был удален. Это может
произойти потому, что пользователь вручную удалил страж, или потому, что
отслеживаемый объект перестал существовать. Мы обсудим это событие в сле­
дующем разделе.
 IN_ISDIR — обрабатываемый объект является каталогом (если это событие не
установлено, обрабатываемый объект является файлом).
 IN_Q_OVERFLOW — переполнение очереди объектов inotify. Ядро ограничивает
размер очереди inotify во избежание неограниченного потребления памяти ядра.
Как только количество ожидающих обработки событий достигает величины на
единицу меньшей, чем максимум, ядро генерирует это событие и ставит его в хвост
очереди. Больше никаких событий не генерируется, пока не произойдет считы­
вание из очереди и ее размер не уменьшится.
 IN_UNMOUNT — устройство, на котором находится отслеживаемый объект, было
отключено от системы (размонтировано), поэтому объект стал недоступен.
В таком случае ядро удаляет страж и генерирует событие IN_IGNORED.

320

Глава 8. Управление файлами и каталогами

Эти события может генерировать любой страж. Пользователю не приходится
задавать их явно.
Программисты должны работать с mask как с битовой маской ожидающих
событий. Следовательно, не проверяйте события с помощью прямых тестов ра­
венства:
/* НЕ ДЕЛАЙТЕ так! */
if (event->mask == IN_MODIFY)
printf ("В файл были записаны данные !\n");
else if (event->mask == IN_Q_OVERFLOW)
printf ("Очередь переполнена!\n);

Вместо этого следует выполнять побитовые тесты:
if (event->mask & IN_ACCESS)
printf ("Из файла были считаны данные!\n");
if (event->mask & IN_UNMOUNTED)
printf ("Отсоединено устройство, которому принадлежал файл!\n);
if (event->mask & IN_ISDIR)
printf ("Файл является каталогом!\n");

Связывание событий перемещения
Каждое из событий IN_MOVED_FROM и IN_MOVED_TO представляет только часть переме­
щения: первое соответствует удалению с указанного места, а второе — прибытию
на новое место. Следовательно, чтобы эта информация была действительно полез­
на для системы, выполняющей интеллектуальное отслеживание перемещений
файлов по системе (допустим, речь идет об индексаторе, который не должен зано­
во индексировать перемещенные файлы), процессы должны иметь возможность
связывать друг с другом два события перемещения.
Рассмотрим поле cookie в структуре inotify_event.
Если поле cookie является ненулевым, оно содержит уникальное значение,
связывающее два события. Допустим, у нас есть процесс, отслеживающий /bin
и /sbin. При этом /bin имеет дескриптор стража 7, а /sbin — дескриптор стража 8.
Если файл /bin/compass перемещается в /sbin/compass, ядро сгенерирует два собы­
тия inotify.
Первое событие будет иметь wd, равный 7, mask, равную IN_MOVED_FROM, и имя
compass. Второе событие будет иметь wd, равный 8, mask, равную IN_MOVED_TO, и имя
compass. У обоих событий cookie будет иметь одно и то же значение — скажем, 12.
Если файл переименовывается, то ядро все равно генерирует два события. Зна­
чение wd у этих событий одинаковое.
Обратите внимание: если файл перемещается в каталог или из каталога, который
не отслеживается, то процесс не получит одно из двух событий (соответствующее).
Сама программа должна заметить, что второе событие с соответствующим cookie
так и не прибудет.

Отслеживание файловых событий

321

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

компонентов является символьной ссылкой, то переход по этой ссылке не про­
исходит и функция inotify_add_watch() не выполняется.
 IN_MASK_ADD — как правило, если вы вызываете inotify_add_watch() для файла,

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

после генерирования первого события для заданного объекта. Фактически
страж получается «одноразовым».
 IN_ONLYDIR — при таком значении страж добавляется к объекту лишь при условии,
что этот объект является каталогом. Если path представляет файл, а не каталог,
то inotify_add_watch() не срабатывает.

Например, следующий фрагмент кода добавляет страж к /etc/init.d, если
init.d является каталогом, причем ни /etc, ни /etc/init.d не является символьной
ссылкой:
int wd;
/*
* Отслеживаем '/etc/init.d' на предмет перемещения, но только если это
* каталог и ни одна часть его пути не является символической ссылкой
*/
wd = inotify_add_watch (fd,
"/etc/init.d",
IN_MOVE_SELF |
IN_ONLYDIR |
IN_DONT_FOLLOW);
if (wd == –1)
perror ("inotify_add_watch");

Удаление стража inotify
Как показано в этом примере, можно удалить страж с экземпляра inotify с помощью
системного вызова inotify_rm_watch():
#include
int inotify_rm_watch (int fd, uint32_t wd);

322

Глава 8. Управление файлами и каталогами

Успешный вызов inotify_rm_watch() удаляет страж, представленный дескрип­
тором wd, от экземпляра inotify, представленного файловым дескриптором fd,
и возвращает 0.
Например:
int ret;
ret = inotify_rm_watch (fd, wd);
if (ret)
perror ("inotify_rm_watch");

При ошибке этот системный вызов возвращает –1 и присваивает errno одно из
следующих значений:
 EBADF — fd не является допустимым экземпляром inotify;
 EINVAL — wd не является допустимым дескриптором стража, сопровождающего
заданный экземпляр inotify.

При удалении стража ядро генерирует событие IN_IGNORED. Ядро отсылает такое
событие не только при удалении вручную, но и когда уничтожение стража явля­
ется побочным эффектом другой операции. Например, если отслеживаемый файл
удаляется, все связанные с ним стражи также удаляются. Во всех подобных слу­
чаях ядро отсылает IN_IGNORED. Такое поведение позволяет приложениям объеди­
нить все операции по удалению стражей в одном месте, а именно в обработчике
событий IN_IGNORED. Такая возможность полезна для комплексных приложений,
управляющих сложными структурами данных, лежащими в основе каждого стра­
жа inotify. Такова, например, поисковая инфраструктура Beagle, применяемая на
рабочем столе GNOME.

Получение размера очереди событий
Размер очереди ожидающих событий можно получить с помощью системного
вызова ioctl на дескрипторе файла экземпляра inotify. Первый аргумент запроса
получает размер очереди в байтах в виде беззнакового целого числа:
unsigned int queue_len;
int ret;
ret = ioctl (fd, FIONREAD, &queue_len);
if (ret < 0)
perror ("ioctl");
else
printf ("Ожидает в очереди %u байтов\n", queue_len);

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

Отслеживание файловых событий

323

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

Уничтожение экземпляра inotify
Для уничтожения экземпляра inotify, а также всех ассоциированных с ним стражей
достаточно закрыть дескриптор экземпляра файла:
int ret;
/* 'fd' был получен с помощью inotify_init() */
ret = close (fd);
if (fd == –1)
perror ("close");

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

Управление 11амятью

Память

-

это один из простейших и в то же время фундаментальных ресурсов,

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

-

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

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

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

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

RAM

(оперативной па­

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

Адресное пространство процесса
Linux, как и все современные операционные системы, виртуализирует имеющуюся
в распоряжении физическую память. Процессы не обращаются непосредственно
к физической памяти. Вместо этого ядро связывает каждый процесс с собственным
уникальным виртуальным адресным nространством данного процесса. Это адрес­
ное пространство является линейНЫМ. Его адреса начинаются с нуля, непрерывно
увеличиваясь вплоть до заданного максимального значения. Кроме того, адресное

пространство является плоским; оно существует в единой области, непосредствен­

но доступной и не требующей сегментирования.

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

-

в страницы. При управлении памятью

Адресное пространство процесса

325

наиболее важной из этих концепций является именно страница. Страница — это
наименьшая адресуемая сущность в памяти, которой может управлять блок управ­
ления памятью (MMU), поэтому можно сказать, что страницы «нарезаются» из
адресного пространства процесса. Размер страницы зависит от применяемой ма­
шинной архитектуры. Наиболее распространенными размерами являются 4 Кбайт
(в 32-битных системах) и 8 Кбайт (в 64-битных системах)1.
В 32-битном адресном пространстве содержится около миллиона страниц раз­
мером по 4 Кбайт каждая. В 64-битном адресном пространстве на несколько по­
рядков больше страниц по 8 Кбайт. Процесс не обязательно будет обращаться ко
всем этим страницам; конкретная страница может ничему не соответствовать.
Страницы могут быть валидными или невалидными. Валидная страница ассоци­
ирована с имеющейся страницей данных, которая располагается либо в физической
памяти (RAM), либо на вторичном носителе (например, в разделе подкачки или
в файле на диске). Невалидная страница ни с чем не ассоциирована и представля­
ет собой неиспользуемый, невыделенный фрагмент адресного пространства.
При обращении к невалидной странице происходит нарушение сегментации.
Если валидная страница ассоциирована с данными, расположенными на допол­
нительном носителе, то процесс не может получить доступ к этой странице, пока
информация не будет перенесена в физическую память. Когда процесс пытается
обратиться к такой странице, блок управления памятью генерирует ошибку страницы. Затем в работу вмешивается ядро, прозрачно подкачивая данные со вторич­
ного носителя в физическую память. Объем виртуальной памяти обычно значи­
тельно превышает объем физической памяти, поэтому ядро может выгрузить данные
из памяти, освободив таким образом пространство для подкачки. Выгрузка — это
процесс переноса данных из физической памяти на вторичный носитель. Чтобы
минимизировать количество последующих операций подкачки, ядро пытается
выгрузить из памяти данные, которые с наименьшей вероятностью будут исполь­
зоваться в ближайшем будущем.
Совместное использование и копирование при записи. Множественные стра­
ницы виртуальной памяти могут быть ассоциированы с единственной физической
страницей, даже если они относятся к разным процессам, каждый из которых
имеет собственное адресное пространство. Таким образом, различные виртуальные
адресные пространства могут совместно использовать (разделять) данные из фи­
зической памяти. Например, в любой момент вполне вероятно, что многие про­
цессы системы совместно используют стандартную библиотеку C. При использо­
вании разделяемой памяти каждый из этих процессов может отображать эту
библиотеку на свое виртуальное адресное пространство, но в физической памяти
при этом должна присутствовать лишь одна копия библиотеки. Более наглядный
пример: два процесса могут одновременно отображать в память большую базу
1

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

326

Глава 9. Управление памятью

данных. В то время как нужная база данных будет присутствовать в виртуальном
адресном пространстве каждого из этих процессов, в оперативной памяти будет
находиться всего одна копия этой базы данных.
Разделяемые данные могут быть доступны только для чтения, записи или пре­
доставляться для одного и другого. Когда процесс записывает информацию на
совместно используемую страницу, допускающую такую запись, может произойти
одна из двух вещей. В простом случае ядро разрешает запись, после чего все про­
цессы, использующие эту страницу, могут видеть результат записи. Как правило,
если мы разрешаем множественным процессам считывать одну и ту же страницу
или записывать на нее информацию, требуется обеспечить некоторую степень
координации и синхронизации между этими процессами, но на уровне ядра запись
«просто работает» и все процессы, разделяющие данные, немедленно видят все
происходящие изменения.
Во втором случае блок управления памятью может перехватить операцию запи­
си и выдать исключение. В ответ ядро прозрачно создает новую копию страницы для
записывающего процесса и позволяет процессу продолжать запись информации уже
на новой странице. Такой подход называется копированием при записи (COW)1.
Фактически процессы получают к разделяемым данным доступ для чтения, благо­
даря чему экономится место. Однако если процесс пытается записать информацию
на разделяемую страницу, то на лету получает уникальную копию этой страницы.
Таким образом, ядро всегда может работать с учетом наличия собственной копии
страницы у каждого записывающего процесса. Копирование при записи происходит
постранично, поэтому такая техника фактически позволяет разделять огромный
файл между многими процессами и отдельные процессы будут получать уникальные
физические копии только страниц, на которые они сами записывают информацию.

Области памяти
Ядро распределяет страницы по блокам, которые имеют набор тех или иных общих
свойств — например, прав доступа. Эти блоки именуются отображениями, сегментами или областями памяти. Некоторые области памяти присутствуют в любом
процессе.
 Текстовый сегмент содержит программный код процесса, строковые литералы,

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

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

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

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

327

 Сегмент данных, или куча, содержит динамическую память процесса. Этот
сегмент доступен для записи, может расширяться или сжиматься. Вызов malloc()

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

переменных находятся специальные значения (как правило, только нули) в со­
ответствии со стандартом C.
Linux оптимизирует эти переменные двумя способами. Во-первых, поскольку bssсегмент предназначен для неинициализированных данных, компоновщик (ld) не
сохраняет специальные значения в объектном файле, поэтому размер двоичного
файла уменьшается. Во-вторых, когда этот сегмент загружается в память, ядро просто
отображает его по принципу копирования при записи на страницу нулей, фактически
устанавливая переменные в значения, которые заданы для них по умолчанию.
ПРИМЕЧАНИЕ
В большинстве адресных пространств содержится несколько отображенных файлов, к которым
относятся, например, сам исполняемый файл программы, библиотека C и другие разделяемые
библиотеки, а также файлы с данными. Взгляните на файл /proc/self/maps или вывод программы
pmap, чтобы получить представление об отображаемых файлах в процессе.

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

Выделение динамической памяти
Память также предоставляется в форме автоматических и статических переменных,
но основа любой системы управления памятью — это выделение, использование и,
наконец, возврат динамической памяти. Динамическая память возвращается во
время выполнения, а не во время компиляции, причем в размере, который может
быть неизвестен вплоть до момента выделения. Разработчик прибегает к исполь­
зованию динамической памяти, когда объем требуемой памяти или длительность
ее использования может варьироваться, причем точные значения размера и дли­
тельности становятся известны только после запуска программы. Например, вы
хотите сохранить в памяти содержимое файла или пользовательского ввода, полу­
ченного через клавиатуру. Размер файла точно не известен, а пользователь может
ввести достаточно много информации, поэтому размер буфера будет варьировать­
ся и вам потребуется его динамически увеличивать по мере считывания все боль­
шего количества данных.
В языке C нет переменной, основой для которой является динамическая память.
Так, в C отсутствует механизм получения структуры struct pirate_ship, существующей
1

Это название сложилось исторически; аббревиатура расшифровывается block started by
symbol — «блок, начинающийся с символа».

328

Глава 9. Управление памятью

в динамической памяти. Вместо этого C предоставляет возможность выделения ди­
намической памяти, достаточной для содержания структуры pirate_ship. После этого
программист будет взаимодействовать с памятью с помощью указателя, в данном
случае struct pirate_ship*.
Классический интерфейс C для получения динамической памяти называется
malloc():
#include
void * malloc (size_t size);

При успешном вызове malloc() выделяет size байт памяти и возвращает указа­
тель в начальную точку только что выделенной области. Содержимое памяти не
определено; не рассчитывайте, что память будет заполнена нулями. При ошибке
malloc() возвращает NULL, а errno устанавливается значение ENOMEM.
Использовать malloc() достаточно просто, как в этом примере, в котором мы
выделяем фиксированное количество байтов:
char *p;
/* дайте мне 2 Kбайт! */
p = malloc (2048);
if (!p)
perror ("malloc");

Или в этом, где выделяется структура:
struct treasure_map *map;
/*
* выделяем достаточное количество памяти для содержания структуры
* treasure_map
* и указываем на нее с помощью 'map'
*/
map = malloc (sizeof (struct treasure_map));
if (!map)
perror ("malloc");

Язык C автоматически повышает указатели на void при любых типах назначения.
Следовательно, в этих примерах мы обходимся без приведения типа возвращаемо­
го значения malloc() к типу lvalue, используемому при назначениях. Однако в язы­
ке C++ не выполняется автоматического повышения указателя на void, поэтому
программисты, имеющие дело с C++, должны приводить тип возвращаемого зна­
чения malloc() следующим образом:
char *name;
/* выделяем 512 байт */
name = (char *) malloc (512);
if (!name)
perror ("malloc");

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

329

Некоторые программисты предпочитают приводить к void тип любого резуль­
тата, получаемого от функции, возвращающей указатель, — в том числе результат
malloc(). Я не рекомендую так поступать, поскольку в данном случае мы скроем
ошибку, если по какой-то причине значение функции изменится и уже не будет
возвращать указатель на void. Более того, такое приведение скроет ошибку и в слу­
чае, если функция была неверно объявлена. Первый из этих ошибочных случаев
маловероятен с функцией malloc(), а второй вполне может произойти.
ПРИМЕЧАНИЕ
Необъявленные функции по умолчанию возвращают int. Превращение целого числа в указатель не
является автоматическим и генерирует предупреждение. Если в данном случае будет выполнено
приведение типа, такое предупреждение не появится.

Функция malloc() может возвращать NULL, поэтому разработчик просто обязан
всегда проверять это условие и обрабатывать ошибки при их наличии. Многие
программы определяют и используют обертку malloc(), выводящую сообщение
об ошибке и завершающие программу, если malloc() возвратит NULL. По традиции
разработчики называют эту распространенную обертку xmalloc():
/* похоже на malloc(), но при ошибке завершается */
void * xmalloc (size_t size)
{
void *p;
p = malloc (size);
if (!p) {
perror ("xmalloc");
exit (EXIT_FAILURE);
}
return p;
}

Выделение массивов
Динамическое выделение памяти также может быть достаточно сложным, если
указанный размер size сам по себе является динамическим. Примером такой рабо­
ты является динамическое выделение массивов, где размер элемента массива может
быть фиксированным, а количество выделяемых элементов переменное. Для упро­
щения данного сценария в библиотеке C предоставляется функция calloc():
#include
void * calloc (size_t nr, size_t size);

При успешном вызове calloc() возвращается указатель на блок памяти, подхо­
дящий для содержания массива из nr элементов, каждый из которых имеет размер
size байт. Следовательно, объем памяти, запрашиваемый при этих двух вызовах,
идентичен (каждый вызов может вернуть больше памяти, чем первоначально за­
прашивал, но не меньше):

330

Глава 9. Управление памятью

int *x, *y;
x = malloc (50 * sizeof (int));
if (!x) {
perror ("malloc");
return –1;
}
y = calloc (50, sizeof (int));
if (!y) {
perror ("calloc");
return –1;
}

Однако их поведение не является идентичным. В отличие от malloc(), не даю­
щего никаких гарантий относительно содержимого выделенной памяти, calloc()
заполняет нулями все байты в возвращаемом фрагменте памяти. Таким образом,
каждый из 50 элементов, содержащихся в массиве целых чисел y, имеет значение
0, а содержимое элементов в x остается неопределенным. Если программа не соби­
рается сразу же установить все 50 значений, то программист должен сам гаранти­
ровать, что элементы в массиве не будут заполнены мусором. Обратите внимание:
двоичный нуль может быть не равен нулю с плавающей точкой!
ПРИМЕЧАНИЕ
Мне не удалось найти оригинальной документации, в которой описывалась бы этимология названия
calloc(). Историки UNIX спорят о происхождении этого названия: возможно, «c» означает count
(«количество»), так как функция принимает количество элементов массива? Может быть, эта буква
означает clear («очистить»), так как функция заполняет память нулями? Выбирайте сторону — дебаты идут ожесточенные.
В поисках истины я обратился к Брайану Кернигану (Brian Kernighan), одному из отцов-основателей
Linux, и спросил, не помнит ли он происхождения этого названия. Брайан оговорился, что исходную
функцию писал не он, но ему кажется, что «с» означает «очистка». Вероятно, это самое авторитетное мнение, на которое можно сослаться.

Часто пользователи стараются «обнулить» динамическую память, даже если не
работают с массивами. Далее в этой главе мы рассмотрим функцию memset(), пре­
доставляющую интерфейс для установки в заданное значение каждого байта в вы­
бранном фрагменте памяти. Однако если мы приказываем calloc() выполнить
заполнение нулями, то работа пойдет быстрее, так как ядро может предоставить
память, уже заполненную нулями.
При ошибке calloc(), как и malloc(), возвращает NULL и устанавливает errno
значение ENOMEM.
Почему стандартизирующие органы так и не определили отдельную от calloc()
функцию «выделить и заполнить нулями»? Это очень странно. Правда, разработ­
чик может с легкостью определить для этого собственный интерфейс:
/* действует так же, как и malloc(), но память заполняется нулями */
void * malloc0 (size_t size)
{

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

331

return calloc (1, size);
}

Мы можем с удобством скомбинировать malloc0() с рассмотренной выше
xmalloc():
/* похожа на malloc(), но заполняет память нулями и завершается при ошибке */
void * xmalloc0 (size_t size)
{
void *p;
p = calloc (1, size);
if (!p) {
perror ("xmalloc0");
exit (EXIT_FAILURE);
}
return p;
}

Изменение размера выделенных областей
Язык C предоставляет интерфейс для изменения размера (увеличения или умень­
шения) уже имеющихся выделенных областей:
#include
void * realloc (void *ptr, size_t size);

При успешном вызове realloc() эта функция изменяет размер области памяти,
на которую направлен указатель ptr, задавая для нее новое значение, равное size
байт. Она возвращает указатель на заново отмеренную область памяти, причем этот
указатель может быть как равен ptr, так и иметь другое значение. В случае увели­
чения области памяти realloc() может оказаться не в состоянии увеличить исход­
ный фрагмент до запрашиваемого размера на том же месте, где этот фрагмент
сейчас находится. В таком случае функция может выделить новую область памяти
размером size байт, скопировать старую область в новую, а старую после этого
высвободить. При любой подобной операции содержимое области памяти сохра­
няется либо полностью, либо в размере, равном объему новой выделенной области.
Поскольку операции realloc() связаны с копированием, при увеличении области
памяти они могут быть довольно затратными.
Если размер size равен нулю, то эффект равносилен вызову free() примени­
тельно к ptr.
Если ptr равен NULL, результат операции аналогичен «свежему» использованию
malloc(). Если указатель ptr не равен нулю, то он обязательно должен быть возвра­
щен в предыдущем вызове, направленном к malloc(), calloc() или realloc().
При ошибке realloc() возвращает NULL и присваивает errno значение ENOMEM.
Состояние области памяти, на которую указывает ptr, остается неизменным.

332

Глава 9. Управление памятью

Рассмотрим пример с уменьшением области памяти. Сначала воспользуемся
calloc() и выделим достаточно памяти, чтобы содержать в ней двухэлементный
массив структур map:
struct map *p;
/* выделяем память для двух структур map */
p = calloc (2, sizeof (struct map));
if (!p) {
perror ("calloc");
return –1;
}
/* используем p[0] и p[1]... */

Теперь предположим, что одно сокровище мы уже нашли и вторая карта (map)
нам больше не требуется, поэтому мы изменим занятую область памяти и отдадим
ее половину, отведенную под карты, обратно в распоряжение системы. Такая опе­
рация не всегда целесообразна, но она полезна, если структура map очень велика,
а оставшуюся карту мы планируем хранить еще достаточно долго:
struct map *r;
/*теперь нам нужна память для хранения только одной карты */
r = realloc (p, sizeof (struct map));
if (!r) {
/* обратите внимание: значение 'p' по-прежнему остается допустимым! */
perror ("realloc");
return –1;
}
/* используем 'r'... */
free (r);

В данном примере p[0] сохраняется после вызова realloc(). Какие бы данные
там ни находились, они остаются доступны. Если вызов вернется с ошибкой, p оста­
ется нетронутым, а значит, действующим. Мы можем продолжать им пользоваться,
а по окончании работы — высвободить. Напротив, если вызов завершится успешно,
мы игнорируем p, а вместо него используем r. Теперь мы отвечаем за высвобожде­
ние r после окончания работы.

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

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

333

как статическим, так и динамическим образом, — автоматически высвобождаются,
как только завершается весь процесс.
Память, выделенная с помощью malloc(), calloc() или realloc(), должна быть
возвращена системе, если эта память больше не используется. Это делается с по­
мощью free():
#include
void free (void *ptr);

Вызов free() освобождает память, на которую указывает ptr. Параметр ptr дол­
жен предварительно быть возвращен malloc(), calloc() или realloc(). Это означает,
что с помощью free() вы не можете высвобождать произвольные фрагменты памя­
ти — например, половину области памяти, — передав указатель на середину выде­
ленного блока. Такое действие приведет к появлению неопределенной памяти, что
проявится в виде сбоя.
Указатель ptr может быть равен NULL, в случае чего free() бесшумно возвращает
значение, поэтому распространенная практика проверки ptr на NULL перед вызовом
free() не имеет смысла.
Рассмотрим пример:
void print_chars (int n, char c)
{
int i;
for (i = 0; i < n; i++) {
char *s;
int j;
/*
* Выделяем и заполняем нулями массив элементов i+2
* состоящий из символов. Обратите внимание: 'sizeof (char)'
* всегда равно 1.
*/
s = calloc (i + 2, 1);
if (!s) {
perror ("calloc");
break;
}
for (j = 0; j < i + 1; j++)
s[j] = c;
printf ("%s\n", s);
/* Все сделано. Можно вернуть память. */
free (s);
}
}

334

Глава 9. Управление памятью

В этом примере мы выделяем n массивов из символов char, содержащих после­
довательно возрастающие количества элементов, начиная от двух (2 байт) до n + 1
элементов (n + 1 байт). Затем, обрабатывая каждый массив, цикл записывает сим­
вол c в каждый байт, кроме последнего (оставляя 0, уже находящийся в последнем
байте). Массив выводится на экран как строка, после чего динамически выделенная
память высвобождается.
При вызове print_chars() с n, равным 5, и c, установленным в X, получаем следу­
ющее:
X
XX
XXX
XXXX
XXXXX

Разумеется, есть и более эффективные способы реализации этой функции.
Однако суть в том, что мы можем динамически выделять и высвобождать память,
даже если размер и количество выделяемых областей становятся известны только
во время выполнения.
ПРИМЕЧАНИЕ
Системы UNIX — в частности, SunOS и SCO — содержат вариант free(), называемый cfree(). В разных системах эта функция может либо работать аналогично free(), либо получать три параметра,
действуя идентично calloc(). В Linux free() может обрабатывать память, полученную от любого
из рассмотренных выше механизмов выделения. Cfree() не следует использовать практически ни
в каких случаях, кроме как для обеспечения обратной совместимости. В Linux cfree() равнозначна free().

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

больше не сможет обратиться к памяти. Такой тип программной ошибки назы­
вается утечкой памяти. Утечки и другие подобные ошибки, связанные с дина­
мической памятью, являются одними из самых распространенных и, к сожале­
нию, относятся к наиболее серьезным неполадкам при программировании на C.
В языке C вся ответственность за управление памятью лежит на плечах про­
граммиста, поэтому он должен очень внимательно следить за операциями вы­
деления памяти.
Еще один распространенный подводный камень при программировании на C —
это так называемое использование освобожденной памяти. Такая уязвимость воз­
никает, если программа обращается к блоку памяти, который уже был освобожден.
Как только для блока памяти вызвана функция free(), программа ни в коем случае
не должна больше обращаться к содержимому этого блока. Программист должен
проявлять особое внимание, отслеживая повисшие указатели: ненулевые, которые
тем не менее указывают на недействительный блок памяти. Отличный инструмент,
помогающий находить в программе ошибки, связанные с неверным использовани­
ем памяти, называется Valgrind.

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

335

Выравнивание
Выравнивание данных — это способ расположения данных в памяти. Об адре­
се A в памяти говорят, что он выровнен по n байт, если n является степенью 2,
а A кратно n. Процессоры, подсистемы памяти и другие системные компоненты
предъявляют специфические требования к выравниванию данных. Например,
большинство процессоров оперируют машинными словами и могут обратиться
к памяти, лишь если она выровнена по размеру слова. Аналогично, как уже упоми­
налось выше, единицы управления памятью работают только с адресами, выров­
ненными по размеру страниц.
Если переменная расположена в памяти по адресу, который кратен ее размеру, то
она называется естественно выровненной. Например, 32-битная переменная являет­
ся естественно выровненной, если она расположена в памяти по адресу, кратно­
му 4, — иными словами, если два самых нижних бита этого адреса равны 0. Соответ­
ственно, тип размером 2n байт должен иметь адрес, в котором n наименьших значимых
битов имеют нулевые значения.
Правила выравнивания обусловлены аппаратным обеспечением, поэтому отли­
чаются от системы к системе. Некоторые машинные архитектуры предъявляют
очень строгие требования к выравниванию данных. В других системах требования
более гибкие. Некоторые системы генерируют улавливаемые ошибки. После этого
ядро может выбрать нужное действие: либо завершить процесс-нарушитель, либо
(что более вероятно) позволить вручную выполнить невыровненный доступ. Как
правило, такой доступ состоит из множества более мелких выровненных операций.
В результате снижается производительность, кроме того, приходится жертвовать
атомарностью, но хотя бы удается не завершать процесс. При написании перено­
симого кода программист должен очень внимательно следить, чтобы не нарушались
требования, связанные с выравниванием.

Выделение выровненной памяти
В большинстве случаев компилятор и библиотека C прозрачно обрабатывают
проблемы, связанные с выравниванием. Стандарт POSIX предписывает, что память,
возвращаемая malloc(), calloc() и realloc(), должна быть правильно выровнена
и пригодна для использования с любыми стандартными типами C. В Linux такие
функции всегда возвращают память, выровненную по 8-байтной границе в 32-бит­
ных системах и по 16-байтной границе в 64-битных.
Иногда программисту нужно, чтобы динамическая память была выровнена по
более широкой границе, например по странице. Мотивы для этого могут быть раз­
ными, но наиболее распространенная причина — необходимость верно выровнять
блоки, используемые при непосредственном блочном вводе-выводе или другом
взаимодействии между аппаратными и программными компонентами. Для этой
цели в POSIX 1003.1d предоставляется функция posix_memalign():
/* одна или другая — достаточно любой */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE

336

Глава 9. Управление памятью

#include
int posix_memalign (void **memptr,
size_t alignment,
size_t size);

Успешный вызов posix_memalign() выделяет size байт динамической памяти,
обеспечивая выравнивание этой памяти по адресу, кратному alignment. Параметр
alignment должен быть степенью 2, а также быть кратным размеру указателя void.
Адрес выделенной памяти помещается в memptr, и вызов возвращает 0.
При ошибке память не выделяется, memptr остается неопределенным, а вызов
возвращает один из следующих кодов ошибок:
 EINVAL — параметр alignment не является степенью 2 или не кратен размеру ука­
зателя void;
 ENOMEM — недостаточно памяти, чтобы выделить запрошенный объем.

Обратите внимание: errno в данном случае не устанавливается — функция воз­
вращает эти ошибки напрямую.
Память, полученная с помощью posix_memalign(), высвобождается посредством
free(). Использовать эту функцию очень просто:
char *buf;
int ret;
/* выделяем 1 Кбайт, выровненный по 256-байтной границе */
ret = posix_memalign (&buf, 256, 1024);
if (ret) {
fprintf (stderr, "posix_memalign: %s\n",
strerror (ret));
return –1;
}
/* используем 'buf'... */
free (buf);

До того как в POSIX была определена функция posix_memalign(), операционные
системы BSD и SunOS предоставляли соответственно следующие интерфейсы:
#include
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);

Функция valloc() аналогична malloc(), за одним исключением: вся выделяемая
память выравнивается по размеру страниц. Как вы помните из гл. 4, применяемый
в системе размер страницы легко получить с помощью getpagesize().
Функция memalign() напоминает posix_memalign. Она выравнивает память по
границе, равной boundary байт, причем это значение должно быть степенью 2.

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

337

В данном примере обе операции выделения возвращают блок памяти, достаточный
для содержания структуры ship, выровненной по размерам страниц:
struct ship *pirate, *hms;
pirate = valloc (sizeof (struct ship));
if (!pirate) {
perror ("valloc");
return –1;
}
hms = memalign (getpagesize (), sizeof (struct ship));
if (!hms) {
perror ("memalign");
free (pirate);
return –1;
}
/* используем 'pirate' и 'hms'... */
free (hms);
free (pirate);

В Linux память, полученная с помощью любой из этих функций, высвобожда­
ется посредством free(). В других UNIX-системах ситуация может быть иной.
Некоторые из них предоставляют механизм для безопасного высвобождения па­
мяти, выделенной с применением этих функций. Если в вашей программе важно
обеспечить переносимость, то у вас может просто не остаться никакой возможности
высвободить память, выделенную через такие интерфейсы!
Специалисты, работающие с Linux, должны использовать эти функции только
для обеспечения переносимости на старые системы; posix_memalign() является
предпочтительным и стандартизированным вариантом. Все три этих интерфейса
необходимы, только если требуется выравнивание с более высокими границами,
чем предоставляются в malloc().

Другие проблемы, связанные с выравниванием
Подобные проблемы не ограничиваются естественным выравниванием стандарт­
ных типов и выделением динамической памяти. Например, с нестандартными
и сложными типами связаны более нетривиальные требования, чем со стандарт­
ными. Кроме того, проблемы выравнивания вдвойне важны, если мы присваива­
ем значения между указателями различных типов и используем при этом приве­
дение типов.
Нестандартные и сложные типы данных предъявляют к выравниванию памяти
такие требования, которые не сводятся к естественному выравниванию. Вот четы­
ре полезных правила работы с ними.
 При работе со структурой ее следует выравнивать по наибольшему из типов, вхо­
дящих в ее состав. Если самым крупным типом в структуре является 32-битное

338

Глава 9. Управление памятью

целое число, выровненное по 4-байтной границе, то структура также должна быть
выровнена как минимум по 4-байтной границе.
 При работе со структурами возникает необходимость заполнения, чтобы гаран­
тировать правильное выравнивание каждого отдельного типа в соответствии
с собственными требованиями данного типа. Таким образом, если оказывается,
что за char (с вероятным выравниванием в 1 байт) следует int (с вероятным
выравниванием в 4 байт), то компилятор вставит между двумя типами три
байта-заполнителя, чтобы обеспечить выравнивание int по 4-байтной границе.
Иногда программисты упорядочивают члены структуры — например, по умень­
шению размера, — чтобы минимизировать пространство, которое впустую рас­
ходуется на заполнители. У gcc есть параметр –Wpadded, который может в этом
помочь. Дело в том, что при его использовании генерируется предупреждение
всякий раз, когда компилятор вставляет неявное заполнение.
 Требование выравнивания объединений: выравнивание происходит по самому
крупному типу, входящему в объединение.
 Требование выравнивания массива: массив выравнивается по базовому типу.
Соответственно, массивы не предъявляют к выравниванию никаких требований,
кроме предъявляемых их типами. В результате происходит естественное вырав­
нивание всех членов массива.
Компилятор прозрачно обрабатывает большинство требований, предъявляемых
к выравниванию; для выявления потенциальных проблем приходится приложить
некоторые усилия. Нередко доводится сталкиваться с проблемами выравнивания
при работе с указателями и приведении.
При обращении к данным через указатель, преобразованный из блока с меньшим
выравниванием в блок с большим, возможна ситуация, в которой процессор будет
загружать информацию, чье выравнивание не соответствует большей границе.
Например, в следующем фрагменте кода при присваивании c переменной badnews
делается попытка прочитать c как unsigned long:
char greeting[] = "Ahoy Matey";
char *c = greeting[1];
unsigned long badnews = *(unsigned long *) c;

Значение unsigned long естественно выравнивается по 4- или 8-байтной границе;
вполне вероятно, что c не будет выравниваться по такой же границе. Следователь­
но, при загрузке c в ходе приведения типов происходит нарушение выравнивания.
В зависимости от машинной архитектуры последствия этого могут быть различ­
ными: от небольшого снижения производительности до аварийного завершения
программы. В машинных архитектурах, позволяющих обнаруживать нарушения
выравнивания, но «не умеющих» их правильно обрабатывать, ядро посылает про­
цессу-нарушителю сигнал SIGBUS, завершающий этот процесс. Подробнее мы пого­
ворим о сигналах в гл. 10.
Подобные примеры более распространены, чем можно было бы подумать. Слу­
чаи, встречающиеся на практике, выглядят не так топорно, но в то же время менее
очевидны.

Управление сегментом данных

339

Строгое перекрытие
В примере с приведением типов также нарушается правило строгого перекрытия — как раз тот аспект C и C++, который не всеми понимается правильно. Строгое перекрытие — это требование, предъявляемое к объекту. При строгом перекрытии обращение к объекту допускается только по точному типу данного
объекта, квалифицированной (например, const или volatile) версии точного типа,
знаковой (или беззнаковой) версии точного типа, через структуру или объединение, в числе членов которой/которого содержится точный тип, либо через указатель char . Например, распространенный способ доступа к uint32_t через два
указателя uint16_t нарушает правило строгого перекрытия.
Вот краткое изложение сути этого правила: разыменование приведения указателя от одного типа переменной к другому типу обычно нарушает правило строгого
перекрытия. Если вы когда-либо сталкивались с предупреждением gcc «dereferencing
type-punned pointer will break strict-aliasing rules» («доступ по указателю с приведением типа нарушает правила перекрытия объектов в памяти»), то строгое перекрытие у вас не выполняется. Строгое перекрытие уже давно входит в состав C++, но
в C это правило было стандартизировано только в C99. Как понятно из сообщения
об ошибке, gcc стимулирует вас придерживаться строгого перекрытия; следуя этому
правилу, вы сможете генерировать оптимальный код.
Для самых любознательных: все тонкости этого правила изложены в разделе 6.5
стандарта ISO C99.

Управление сегментом данных
Исторически в системах UNIX предоставлялись интерфейсы для непосредствен­
ного управления сегментом данных. Тем не менее в большинстве программ эта
возможность практически не находила применения, так как malloc() и другие спо­
собы выделения памяти значительно проще в использовании, а также мощнее.
Я опишу подобные интерфейсы для полноты картины, а также для немногочис­
ленных читателей, которые собираются реализовать собственный механизм выде­
ления памяти на основе работы с кучей:
#include
int brk (void *end);
void * sbrk (intptr_t increment);

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

340

Глава 9. Управление памятью

Вызов brk() устанавливает точку останова (конец сегмента данных), задавая
адрес, указанный в end. В случае успеха функция возвращает 0. При ошибке она
возвращает –1 и устанавливает errno значение ENOMEM.
Вызов sbrk() приращивает конец сегмента данных на increment байт, причем
дельта может быть как положительной, так и отрицательной. Sbrk() возвращает
пересмотренную точку останова. Соответственно, приращение, равное 0, дает нам
актуальную точку останова:
printf ("Текущая точка останова — %p\n", sbrk (0));

Ни в стандарте C, ни в POSIX намеренно не определяется ни одна из этих
функций. Однако практически во всех UNIX-системах поддерживается как мини­
мум одна из них, а то и две. Переносимые программы должны работать только со
стандартизированными интерфейсами.

Анонимные отображения в памяти
При выделении памяти в glibc используется комбинация сегмента данных и отобра­
жений в памяти. Классический метод реализации malloc() — разделение сегмента
данных на последовательность фрагментов, размер которых является степенью 2;
после этого запросы на выделение памяти удовлетворяются путем возврата фрагмен­
та, который максимально соответствует запрошенному размеру. Высвободить память
не составляет труда: достаточно просто пометить фрагмент как свободный. Если
смежные фрагменты свободны, их можно объединить в один более крупный фрагмент.
Если верхняя часть кучи совершенно свободна, то си­стема может понизить точку
останова с помощью brk(), уменьшив таким образом кучу и вернув память ядру.
Этот алгоритм называется схемой дружеского выделения памяти. К его до­
стоинствам относятся высокая скорость работы и простота, а основным недо­
статком является возникновение двух типов фрагментации. Внутренняя фрагментация происходит, когда для удовлетворения запроса выделяется больше
памяти, чем было запрошено. В результате доступная память расходуется не­
эффективно. Внешняя фрагментация возникает, когда в наличии есть достаточ­
но свободной памяти для удовлетворения запроса, но эта свободная память
разбита на два и более несмежных фрагмента. В таком случае память также
расходуется неэффективно (так как может использоваться сравнительно круп­
ный блок, не слишком подходящий в данном случае). Кроме того, возрастает
количество ошибок при выделении памяти (если в наличии не оказывается
альтернативного блока).
Более того, при такой схеме одна операция выделения памяти может «зацеплять»
другую, что не позволяет библиотеке C возвращать высвободившуюся память в ядро.
Предположим, было выделено два блока памяти: A и B. Блок A вплотную приле­
гает к точке останова, а блок B находится непосредственно под A. Даже если про­
грамма освободит блок B, библиотека C не сможет откорректировать точку оста­
нова, пока таким же образом не будет освобожден фрагмент A. Следовательно,

Анонимные отображения в памяти

341

долгоживущая выделенная область может удерживать в памяти и другие, уже
пустые области.
Данная ситуация не всегда является проблемой, поскольку библиотеки C не
обязаны возвращать память в систему. Как правило, куча не уменьшается после
каждого высвобождения. Вместо этого реализация malloc() придерживает память
до следующего выделения. Только если размер кучи значительно превышает объ­
ем выделенной памяти, malloc() действительно сужает сегмент данных, но выделе­
ние большого фрагмента может препятствовать такому сужению.
Следовательно, при крупных выделениях памяти glibc не использует кучу.
Вместо этого она создает анонимное отображение в памяти, с помощью которого
удовлетворяет запрос на выделение. Анонимные отображения в памяти подобны
файловым отображениям, рассмотренным в гл. 4, не считая того, что в их основе
не лежит какой-либо файл — поэтому они и называются анонимными. На самом
деле анонимное отображение в памяти — это просто большой блок памяти, запол­
ненный нулями и готовый к использованию. Можете считать его новоиспеченной
кучей, которая будет использоваться в рамках одного выделения в памяти. Такие
отображения располагаются вне кучи, поэтому не приводят к дополнительной
фрагментации сегмента данных.
При выделении памяти методом анонимных отображений мы имеем следующие
преимущества.
 Отсутствуют проблемы, связанные с фрагментацией. Когда программа больше

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

права доступа к ним, они могут получать извещения, как и обычные отображе­
ния (см. гл. 4).
 Каждое выделение существует в отдельном отображении в памяти. Нет необ­

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

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

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

342

Глава 9. Управление памятью

к анонимным отображениям в памяти. Этот порог является нежестким (см. разд. «Рас­
ширенное выделение памяти» далее) и может изменяться от одного релиза glibc
к другому. В настоящее время он равен 128 Кбайт: выделения памяти, не превыша­
ющие этого объема, делаются из кучи, а при более крупных применяются анонимные
отображения в памяти.

Создание анонимных отображений
в памяти
Может быть, при каком-то выделении вы хотите принудительно использовать
отображение в памяти, а не кучу; возможно, вы пишете собственную систему вы­
деления памяти. В этих случаях потребуется вручную создавать отображения
в памяти. Так или иначе, в Linux это не составляет труда. Как вы помните из гл. 4,
системный вызов mmap() создает отображение в памяти, а системный вызов munmap()
его уничтожает.
#include
void * mmap (void *start,
size_t length,
int prot,
int flags,
int fd,
off_t offset);
int munmap (void *start, size_t length);

Кстати, создать анонимное отображение в памяти проще, чем отображение,
в основе которого лежит файл, поскольку в первом случае не приходится ни от­
крывать файл, ни управлять им. Основное отличие заключается в присутствии
специального флага, указывающего, что отображение является анонимным.
Рассмотрим пример:
void *p;
p = mmap (NULL,
512 * 1024,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
–1,
0);

/*
/*
/*
/*
/*
/*

неважно где */
512 Кбайт */
чтение/запись */
анонимное, приватное */
fd (игнорируется) */
смещение (игнорируется) */

if (p == MAP_FAILED)
perror ("mmap");
else
/* 'p' указывает на 512 Кбайт анонимной памяти... */

Анонимные отображения в памяти

343

При большинстве анонимных отображений параметры mmap() соответствуют
этому примеру, за исключением, разумеется, передачи произвольного размера
(в байтах), определяемого программистом. Другие параметры обычно таковы.
 Первый параметр, start, устанавливается в значение NULL, указывая, что ано­

нимное отображение может начинаться в любой точке памяти, выбранной ядром.
Здесь можно задать и ненулевое значение, если оно выровнено по границам
страниц, но это ограничивает переносимость. Обычно для программы не имеет
значения, где именно в памяти находится отображение.
 Параметр prot обычно одновременно задает биты PROT_READ и PROT_WRITE, допус­

кая как считывание отображения, так и запись в него. С другой стороны, вы­
полнение кода из анонимного отображения обычно нежелательно, так как это
возможный вектор для атаки злоумышленников.
 Параметр flags задает флаг MAP_ANONYMOUS, делая данное отображение анонимным,
а также флаг MAP_PRIVATE, делая отображение приватным.
 Параметры fd и offset игнорируются, если задан флаг MAP_ANONYMOUS. Однако
в некоторых старых системах предполагается, что в данном случае fd должен
иметь значение –1, поэтому такое значение целесообразно задавать для обеспе­

чения переносимости.
Память, полученная с помощью анонимного отображения, выглядит как память,
взятая из кучи. Одно из достоинств выделения памяти именно из анонимных ото­
бражений заключается в том, что мы получаем готовые страницы, уже заполненные
нулями, причем без каких-либо издержек, так как ядро отображает анонимные
страницы приложения на страницы в памяти посредством копирования при записи.
Следовательно, не приходится применять вызов memset() к возвращенной памяти.
В этом и заключается одно из достоинств использования calloc() по сравнению
с malloc(), за которым следует memset(): glibc известно, что анонимные отображения
уже заполнены нулями и что calloc(), удовлетворяемый из отображения, не требу­
ет явного заполнения такого типа.
Системный вызов munmap() высвобождает анонимное отображение, возвращая
выделенную память ядру:
int ret;
/* с 'p' все готово, отдаем отображение в 512 Кбайт обратно */
ret = munmap (p, 512 * 1024);
if (ret)
perror ("munmap");
ПРИМЕЧАНИЕ
Подробнее об использовании mmap(), munmap() и отображений в целом было рассказано
в гл. 4.

344

Глава 9. Управление памятью

Отображение /dev/zero
В других UNIX-системах, в частности BSD, флаг MAP_ANONYMOUS отсутствует. Вместо
этого в них применяется схожее решение, связанное с отображением особого фай­
ла устройства /dev/zero. Этот файл устройства обеспечивает такую же семантику,
как и анонимная память. Отображение содержит скопированные при записи стра­
ницы, которые заранее заполнены нулями. Соответственно, с практической точки
зрения оно не отличается от анонимной памяти.
В Linux всегда предоставлялось устройство /dev/zero, а также возможность его
отображения и получения памяти, заполненной нулями. На самом деле до появле­
ния флага MAP_ANONYMOUS программисты Linux пользовались именно таким подходом.
Для обеспечения обратной совместимости со сравнительно старыми версиями Linux
либо переносимости на другие UNIX-системы разработчик по-прежнему может
использовать /dev/zero вместо создания анонимного отображения. Синтаксис не
отличается от операции отображения любого другого файла:
void *p;
int fd;
/* открываем /dev/zero для чтения и записи */
fd = open ("/dev/zero", O_RDWR);
if (fd < 0) {
perror ("open");
return –1;
}
/* отображаем [0, размер страницы) из /dev/zero */
p = mmap (NULL,
/* неважно где */
getpagesize (),
/* отображаем одну страницу */
PROT_READ | PROT_WRITE, /* отображаем для чтения/записи */
MAP_PRIVATE,
/* приватное отображение */
fd,
/* отображаем /dev/zero */
0);
/* без смещения */
if (p == MAP_FAILED) {
perror ("mmap");
if (close (fd))
perror ("close");
return –1;
}
/* закрываем /dev/zero, он нам больше не нужен */
if (close (fd))
perror ("close");
/* 'p' указывает на одну страницу в памяти, используем ее... */

Разумеется, память, отображаемая таким образом, высвобождается с помощью
munmap().

Расширенное выделение памяти

345

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

Расширенное выделение памяти
Многие операции выделения памяти, рассматриваемые в этой главе, ограничены
и управляются glibc или параметрами ядра, которые программист может менять.
Для этого применяется вызов mallopt():
#include
int mallopt (int param, int value);

Вызов mallopt() устанавливает параметр, связанный с управлением памятью
(называемый param) в значение value. В случае успеха вызов возвращает ненулевое
значение, при ошибке — 0. Обратите внимание: mallopt() не устанавливает errno.
Кроме того, этот вызов практически всегда оканчивается успешно, поэтому не рас­
считывайте, что сможете что-то прояснить на основе возвращенного значения.
В настоящее время Linux поддерживает семь значений param, все они определя­
ются в .
 M_CHECK_ACTION — значение переменной окружения MALLOC_CHECK_ (будет рассмот­

рена в следующем разделе).
 M_MMAP_MAX — максимальное количество отображений, которые система может

создать для удовлетворения запросов динамической памяти. Когда лимит до­
стигнут, сегмент данных будет использоваться для всех операций выделения
памяти, пока не будет высвобождено какое-либо из ранее созданных отображе­
ний. Значение 0 полностью отменяет использование любых анонимных отобра­
жений для выделения динамической памяти.
 M_MMAP_THRESHOLD — порог (измеряемый в байтах), вплоть до которого запросы

на выделение памяти будут удовлетворяться с помощью анонимного отобра­
жения, без участия сегмента данных. Обратите внимание: выделения меньше
этого порога также могут удовлетворяться посредством анонимных отобра­
жений по усмотрению системы. Значение 0 активирует использование ано­
нимных отображений во всех случаях, фактически отменяя применение сег­
мента данных при выделениях динамической памяти.
 M_MXFAST — максимальный размер быстрой корзины (в байтах). Быстрые корзины

(fast bins) — это особые фрагменты памяти в куче, которые никогда не объеди­
няются со смежными областями и не возвращаются в систему. Таким образом,
работая с быстрыми корзинами, мы добиваемся очень оперативного выделения
памяти за счет увеличения фрагментации. Значение 0 деактивирует любое
использование быстрых корзин.
 M_PERTURB — активирует искажение памяти, помогающее выявлять ошибки
в управлении памятью. Получая ненулевое значение value, glibc устанавливает

346

Глава 9. Управление памятью

все выделенные байты (кроме тех, которые были запрошены через calloc())
в значение, логически дополняющее наименьший значимый байт в value. Это
помогает обнаруживать ошибки, связанные с использованием до инициализа­
ции. Более того, glibc устанавливает все высвобожденные байты в наименьший
значимый байт из value. Так удается идентифицировать ошибки, связанные
с использованием после высвобождения.
 M_TOP_PAD — объем заполнения (в байтах), используемого при корректировке раз­
мера сегмента данных. Всякий раз, когда glibc задействует brk() для увеличения
сегмента данных, вы можете запросить больше памяти, чем требуется, рассчитывая
обойтись без дополнительного вызова brk(), который понадобился бы для полу­
чения новой порции памяти в ближайшем будущем. Аналогично всякий раз, когда
glibc уменьшает размер сегмента данных, она может удержать дополнительную
память, отдав немного меньше, чем требовалось. Эти дополнительные байты и на­
зываются заполнением. Значение 0 отключает любое использование заполнения.
 M_TRIM_THRESHOLD — минимальное количество свободной памяти (в байтах), ко­
торое находится поверх сегмента данных перед тем, как glibc выполнит вызов
sbrk() для возврата памяти в ядро.
Стандарт XPG, в свободной форме определяющий mallopt(), указывает три
других параметра: M_GRAIN, M_KEEP и M_NLBLKS. Linux определяет эти параметры, но,
задавая для них значения, мы ничего не добьемся. В табл. 9.1 приведен полный
список всех допустимых параметров, значений, задаваемых для них по умолчанию,
а также диапазон их допустимых значений.
Таблица 9.1. Параметры mallopt()
Параметр

Происхождение

M_CHECK_ACTION Linux-специ­фичный
M_GRAIN
Стандарт XPG

По умолчанию
0
Не поддерживается в Linux
Не поддерживается в Linux
64 * 1024

Допустимые
значения
0–2

Специальные
значения

≥0

0 отключает
использование
mmap()
0 отключает
использование
кучи
0 отключает
использование
быстрых корзин

M_KEEP

Стандарт XPG

M_MMAP_MAX

Уникальный
для Linux

M_MMAP_
THRESHOLD

Уникальный
для Linux

128 * 1024

≥0

M_MXFAST

Стандарт XPG

64

0 - 80

M_NLBLKS

Стандарт XPG

M_PERTURB

Уникальный
для Linux
Уникальный
для Linux
Уникальный
для Linux

Не поддерживается в Linux
0

0 или 1

0

≥0

128 * 1024

≥ –1

M_TOP_PAD
M_TRIM_
THRESHOLD

0 отключает пертурбацию
0 отключает заполнение сверху
–1 отключает
отсечение

Расширенное выделение памяти

347

Любые вызовы mallopt() в программе должны происходить до первого вызова
malloc() или другого интерфейса выделения памяти. Использовать их просто:
int ret;
/* используем mmap() для всех операций выделения больше 64 Кбайт */
ret = mallopt (M_MMAP_THRESHOLD, 64 * 1024);
if (!ret)
fprintf (stderr, "mallopt failed!\n");

Тонкая настройка с помощью malloc_usable_size() и malloc_trim(). Linux
предоставляет пару функций, обеспечивающих низкоуровневое управление сис­
темой выделения памяти в glibc. Первая такая функция позволяет запрашивать,
сколько пригодных для использования байтов содержится в указанном выделенном
фрагменте памяти:
#include
size_t malloc_usable_size (void *ptr);

Успешный вызов malloc_usable_size() возвращает точный размер выделяемого
фрагмента памяти, на который направлен указатель ptr. Поскольку glibc иногда
округляет выделяемые области, чтобы они умещались в имеющийся фрагмент
анонимного отображения, рабочий объем такой области может быть больше, чем
запрошено. Разумеется, выделенная область не может быть меньше, чем запроше­
но. Вот пример использования этой функции:
size_t len = 21;
size_t size;
char *buf;
buf = malloc (len);
if (!buf) {
perror ("malloc");
return –1;
}
size = malloc_usable_size (buf);
/* на самом деле в нашем распоряжении есть 'size' байтов из буфера 'buf' ... */

Вторая функция позволяет программе принудить glibc к тому, чтобы вся память,
которую компилятор может высвободить в данный момент, сразу же вернулась
к ядру:
#include
int malloc_trim (size_t padding);

При успешном вызове malloc_trim() сегмент данных максимально укорачива­
ется, без учета байтов padding, которые резервируются. В таком случае функция
возвращает 1. При ошибке она возвращает 0. Как правило, glibc выполняет такое

348

Глава 9. Управление памятью

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

Отладка при операциях выделения памяти
Программы могут устанавливать переменную окружения MALLOC_CHECK_, обеспечи­
вающую перевод подсистемы памяти в режим отладки. Дополнительные отладоч­
ные проверки осуществляются за счет снижения эффективности выделения памя­
ти, но на этапе отладки в ходе разработки приложения эти издержки оправданны.
Отладкой управляет переменная окружения, поэтому нет необходимости пере­
компилировать программу. Например, можно просто выдать следующую команду:
$ MALLOC_CHECK_=1 ./rudder

Если MALLOC_CHECK_ установлена в 0, то подсистема памяти бесшумно игнориру­
ет все ошибки. Если здесь мы имеем значение 1, то в stderr выводится информа­
тивное сообщение. Если переменная получит значение 2, программа немедленно
завершается с помощью abort(). Поскольку MALLOC_CHECK_ изменяет поведение ра­
ботающей программы, программы setuid игнорируют эту переменную.
Получение статистической информации. В Linux предоставляется функция
mallinfo(), выдающая статистическую информацию о системе выделения памяти:
#include
struct mallinfo mallinfo (void);

Вызов mallinfo() возвращает статистическую информацию в структуре mallinfo.
Эта структура возвращается по значению, а не по указателю. Ее содержимое также
определяется в :
/* все размеры указаны в байтах */
struct mallinfo {
int arena;
int ordblks;
int smblks;
int hblks;
int hblkhd;
int usmblks;
int fsmblks;
int uordblks;
int fordblks;
int keepcost;
};

/*
/*
/*
/*
/*
/*
/*
/*
/*
/*

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

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

349

Работать с ней просто:
struct mallinfo m;
m = mallinfo ();
printf ("free chunks: %d\n", m.ordblks);

В Linux также предоставляется функция malloc_stats(), выводящая в stderr
статистку, относящуюся к памяти:
#include
void malloc_stats (void);

При вызове malloc_stats() в программе, интенсивно потребляющей память,
получаются довольно большие числа:
Arena 0:
system bytes
= 865939456
in use bytes
= 851988200
Total (incl. mmap):
system bytes
= 3216519168
in use bytes
= 3202567912
max mmap regions = 65536
max mmap bytes
= 2350579712

Выделение памяти на основе стека
До сих пор мы обсуждали механизмы выделения динамической памяти, которые
использовали для получения памяти кучу или отображения. Это неудивительно,
поскольку куча и отображения в памяти являются динамическими по своей природе.
В адресном пространстве процесса обычно находится еще одна программная кон­
струкция — стек, в котором располагаются автоматические переменные программы.
Правда, программист вполне может использовать стек для выделения динамической
памяти. Пока такое выделение не вызывает переполнения стека, подобный подход
должен оставаться простым и осуществляться без проблем. Чтобы выполнить выде­
ление динамической памяти из стека, пользуйтесь системным вызовом alloca():
#include
void * alloca (size_t size);

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

350

Глава 9. Управление памятью

Эта функция используется идентично malloc(), но вам не требуется высвобо­
ждать выделенную память (на самом деле этого и нельзя делать). Ниже приведен
пример функции, которая открывает указанный файл в конфигурационном ката­
логе системы — вероятно, это каталог /etc, — который машинонезависимо опреде­
ляется во время компиляции. Функция должна выделить новый буфер, скопировать
в него конфигурационный каталог системы, а потом сцепить этот буфер с предо­
ставленным именем файла:
int open_sysconf (const char *file, int flags, int mode)
{
const char *etc = SYSCONF_DIR; /* "/etc/" */
char *name;
name = alloca (strlen (etc) + strlen (file) + 1);
strcpy (name, etc);
strcat (name, file);
}

return open (name, flags, mode);

После возврата память, выделенная с помощью функции alloca(), автоматиче­
ски высвобождается, поскольку стек возвращается к вызывающей функции. Таким
образом, после возврата функции, вызвавшей alloca(), вы уже не можете исполь­
зовать эту память! Тем не менее, поскольку вам не приходится выполнять какуюлибо очистку (вызывать free()), результирующий код получается немного акку­
ратнее. Вот та же функция, реализованная с помощью malloc():
int open_sysconf (const char *file, int flags, int mode)
{
const char *etc = SYSCONF_DIR; /* "/etc/" */
char *name;
int fd;

}

}

name = malloc (strlen (etc) + strlen (file) + 1);
if (!name) {
perror ("malloc");
return –1;
strcpy (name, etc);
strcat (name, file);
fd = open (name, flags, mode);
free (name);
return fd;

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

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

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

351

/* НЕ ДЕЛАЙТЕ ТАК! */
ret = foo (x, alloca (10));

У интерфейса alloca() довольно темная история. Во многих системах он ра­
ботал неправильно или провоцировал непредсказуемое поведение. В системах
с небольшим стеком и стеком, имеющим фиксированный размер, использование
alloca() приводило к переполнению стека и аварийному завершению программы.
В некоторых других системах alloca() просто не существовал. Со временем пол­
ные ошибок и непоследовательные реализации окончательно подмочили репу­
тацию alloca().
Итак, если вы стремитесь обеспечить переносимость вашей программы, то ста­
райтесь не использовать alloca(). Тем не менее в рамках Linux alloca() является
превосходным и недооцененным инструментом. Он работает исключительно хоро­
шо — во многих архитектурах при выделении памяти с помощью alloca() приходит­
ся всего лишь повысить указатель стека. Этот вызов оказывается удобнее и произ­
водительнее, чем malloc(). При выделении небольших объемов памяти в уникальном
для Linux коде alloca() позволяет радикально улучшить рабочие характеристики
системы.

Дублирование строк в стеке
Один из самых распространенных случаев использования alloca() связан с вре­
менным дублированием строки. Например:
/* мы хотим дублировать 'song' */
char *dup;
dup = alloca (strlen (song) + 1);
strcpy (dup, song);
/* bcgjkmpetv 'dup'... */
return; /* 'dup' автоматически высвобождается */

Такая операция требуется довольно часто, а также, учитывая существенное
ускорение работы при применении alloca(), в системах Linux предоставляются
варианты strdup(), дублирующие указанную строку в стеке:
#define _GNU_SOURCE
#include
char * strdupa (constchar *s);
char * strndupa (const char *s, size_t n);

Вызов strdupa() возвращает дубликат s. При вызове strndupa() дублируется до
n символов из s. Если s длиннее n, то дублирование прекращается на n и функция
прикрепляет нулевой байт. Эти функции обладают всеми достоинствами alloca().
Дублированная строка автоматически высвобождается, как только возвращается
функция, сделавшая вызов.

352

Глава 9. Управление памятью

Стандарт POSIX не определяет функций alloca(), strdupa() или strndupa(),
в других операционных системах они встречаются эпизодически. Если важно
обеспечить переносимость программы, то использовать эти функции настоятельно
не рекомендуется. Однако в Linux alloca() и ее аналоги работают вполне неплохо
и помогают значительно повысить производительность, избавляя вас от хитрых
манипуляций с выделением динамической памяти, — достаточно просто откоррек­
тировать указатель на кадр стека.

Массивы переменной длины
В C99 появились массивы переменной длины (VLA). Это массивы, геометрия кото­
рых задается во время выполнения, а не компиляции. В GNU C массивы перемен­
ной длины уже поддерживались некоторое время, но после их стандартизации в C99
появилось больше стимулов для их использования. Массивы переменной длины
позволяют избежать издержек, связанных с выделением динамической памяти,
почти по такому же принципу, что и alloca().
Они работают именно так, как вы и предполагали:
for (i = 0; i < n; ++i) {
char foo[i + 1];
/* используем 'foo'... */
}

Здесь foo — это массив символов char, имеющий переменный размер i + 1. При каж­
дой итерации цикла foo создается динамически и автоматически очищается, как
только он оказывается за пределами области определения. Если вместо VLA мы
воспользуемся alloca(), то память не будет освобождена вплоть до возврата функции.
Массив переменной длины гарантирует, что высвобождение памяти происходит при
каждой итерации цикла. Соответственно, используя массив переменной длины, мы
потребим не более n байт, тогда как alloca() потребуется n * (n + 1) / 2 байт.
Применив массив переменной длины, мы можем переписать нашу функцию
open_sysconf() вот так:
int open_sysconf (const char *file, int flags, int mode)
{
const char *etc; = SYSCONF_DIR; /* "/etc/" */
char name[strlen (etc) + strlen (file) + 1];
strcpy (name, etc);
strcat (name, file);
return open (name, flags, mode);
}

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

353

Выбор механизма выделения памяти

в распоряжении до выхода содержащей ее переменной из области определения; а та­
кой выход может наступить до возврата функции. Это может иметь как положи­
тельные, так и отрицательные последствия. В цикле for, который мы только что
рассмотрели, высвобождение памяти при каждой итерации цикла снижает общий
объем потребляемой памяти без каких-либо побочных эффектов (нам не требует­
ся держать на подхвате дополнительную память). Однако если мы по какой-то
причине хотим, чтобы память имелась в наличии дольше, чем на протяжении од­
ного цикла, то целесообразно использовать alloca().
ПРИМЕЧАНИЕ
При смешивании alloca() и массивов переменной длины в рамках одной функции можно спровоцировать неожиданное поведение. Избегайте риска и используйте в конкретной функции или одно,
или другое.

Выбор механизма выделения памяти
В этой главе мы рассмотрели множество операций, связанных с выделением памя­
ти. У программиста вполне может возникнуть вопрос: какое решение оптимально
для конкретной задачи? В большинстве случаев вы не прогадаете, если восполь­
зуетесь malloc(). Тем не менее в некоторых ситуациях целесообразен иной подход.
В табл. 9.2 обобщены рекомендации по выбору механизма выделения памяти.
Таблица 9.2. Способы выделения памяти в Linux
Способ
выделения
malloc()
calloc()

realloc()
brk() и sbrk()
Анонимные выделения памяти

posix_memalign()

memalign()
и valloc()

За

Против

Легкий, распространенный

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

354

Глава 9. Управление памятью

Таблица 9.2 (продолжение)
Способ
выделения
alloca()

Массивы переменной длины

За

Против

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

Не может вернуть ошибку, неудобен
при выделении больших областей,
не работает в некоторых UNIX-си­
стемах
Полезны только при работе с массивами; в некоторых ситуациях
высвобождающее поведение alloca()
может быть предпочтительнее; менее распространена в UNIX-системах,
чем alloca()

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

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

Установка байтов
Из функций для управления памятью чаще всего применяется memset():
#include
void * memset (void *s, int c, size_t n);

Вызов memset() устанавливает n байт, начиная с s, в байт c и возвращает s. Часто
эта функция используется для заполнения блока памяти нулями:
/* обнуляем [s,s+256) */
memset (s, '\0', 256);

Функция bzero() — это ранний, устаревающий интерфейс, появившийся в си­
стеме BSD для решения аналогичной задачи. В новом коде следует использовать

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

355

memset(), но Linux предоставляет bzero() для обеспечения обратной совместимости

и переносимости на другие системы:
#include
void bzero (void *s, size_t n);

Следующий вызов аналогичен приведенному выше примеру с memset():
bzero (s, 256);

Обратите внимание: bzero() (наряду с другими b-интерфейсами) требует заго­
ловка , а не .
ВНИМАНИЕ
Избегайте выделения памяти с помощью malloc(), если после этого собираетесь сразу заполнить
полученную память нулями посредством memset(). Да, вы получите желаемый результат, но нерационально вызывать две функции для достижения такого же эффекта, что от одной calloc(),
которая также возвращает заполненную нулями память. Так вы не просто сэкономите один вызов
функции; ведь calloc() может получить уже заполненную нулями память из ядра. В таком случае
вам не придется вручную устанавливать каждый байт в 0, что положительно сказывается на производительности.

Сравнение байтов
Аналогично strcmp() memcmp() проверяет два фрагмента памяти на эквивалент­
ность:
#include
int memcmp (const void *s1, const void *s2, size_t n);

Вызов сравнивает первые n байт s1 с первыми n байт в s2, после чего возвраща­
ет 0, если блоки памяти эквивалентны. Если s1 меньше s2, то возвращается значение
меньше 0, а если s1 больше s2, то возвращается значение больше 0.
BSD, в свою очередь, предлагает интерфейс, который в настоящее время уже
выходит из употребления, но предназначен для выполнения примерно таких же
задач:
#include
int bcmp (const void *s1, const void *s2, size_t n);

При вызове bcmp() сравниваются первые n байт в s1 и s2. Если блоки памяти
эквивалентны, то возвращается нулевое значение, если неэквивалентны — то не­
нулевое.
В силу применения структур данных (см. подразд. «Выравнивание» разд. «Выде­
ление динамической памяти» в этой главе), сравнение двух структур на эквивалент­
ность с помощью memcmp() или bcmp() — неточная операция. При заполнении может
применяться неинициализированный мусор, состав которого у двух экземпляров

356

Глава 9. Управление памятью

структуры отличается, тогда как в остальном эти структуры идентичны. Следователь­
но, приведенный далее код является небезопасным:
/* идентичны ли две шлюпки? (НЕБЕЗОПАСНО) */
int compare_dinghies (struct dinghy *a, struct dinghy *b)
{
return memcmp (a, b, sizeof (struct dinghy));
}

Если программист собирается сравнивать структуры, то нужно сравнивать друг
с другом каждый элемент этих структур по очереди. Такой поход допускает неко­
торую оптимизацию, но определенно является более трудоемким, чем небезопасная
работа с применением memcmp(). Вот эквивалентный код:
/* Идентичны ли две шлюпки? */
int compare_dinghies (struct dinghy *a, struct dinghy *b)
{
int ret;
if (a->nr_oars < b->nr_oars)
return –1;
if (a->nr_oars > b->nr_oars)
return 1;
ret = strcmp (a->boat_name, b->boat_name);
if (ret)
return ret;
/* и т. д. для каждого элемента... */
}

Перемещение байтов
Функция memmove() копирует первые n байт из src в dst, возвращая dst:
#include
void * memmove (void *dst, const void *src, size_t n);

Опять же BSD предоставляет выходящий из употребления интерфейс для ре­
шения аналогичной задачи:
#include
void bcopy (const void *src, void *dst, size_t n);

Обратите внимание: обе функции принимают одинаковые параметры, однако
порядок первых двух в bcopy() является обратным.
И bcopy(), и memmove() позволяют безопасно обрабатывать пересекающиеся об­
ласти памяти (например, если часть dst находится внутри src). Таким образом, мы
можем, например, перемещать байты в определенной области памяти вверх-вниз.
Данная ситуация встречается редко, а программисту необходимо знать о ее насту­

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

357

плении, поэтому стандарт C определяет вариант memmove(), не поддерживающий
такого взаимного пересечения областей памяти. Потенциально этот вариант рабо­
тает быстрее:
#include
void * memcpy (void *dst, const void *src, size_t n);

Функция работает аналогично memmove(), с оговоркой, что пересечение dst и src
не разрешается. Если это происходит, то результат не определен.
Еще одна безопасная функция для копирования называется memccpy():
#include
void * memccpy (void *dst, const void *src, int c, size_t n);

Функция memccpy() работает так же, как и memcpy(), но копирование останавлива­
ется, если функция находит байт c среди первых n байт в src. Вызов возвращает
указатель на байт, который следует в dst после c, либо NULL, если c не был найден.
Наконец, можно использовать mempcpy() для пошагового прохода через па­
мять:
#define _GNU_SOURCE
#include
void * mempcpy (void *dst, const void *src, size_t n);

Функция mempcpy() работает так же, как и memcpy(), но она возвращает указатель
на следующий байт после последнего скопированного байта. Она полезна, если
множество данных требуется скопировать в последовательно расположенные об­
ласти памяти. Однако улучшение незначительное, так как возвращаемое значе­
ние — это просто сумма dst + n. Эта функция является уникальной для GNU.

Поиск байтов
Функции memchr() и memrchr() находят заданный байт в блоке памяти:
#include
void * memchr (const void *s, int c, size_t n);

Функция memchr() просматривает n байт в области памяти, на которую направлен
указатель s, и ищет символ c, интерпретируемый как unsigned char:
#define _GNU_SOURCE
#include
void * memrchr (const void *s, int c, size_t n);

Вызов возвращает указатель на первый байт, совпадающий с c, либо NULL, если
c не найден.

358

Глава 9. Управление памятью

Функция memrchr() равнозначна memchr(), но она выполняет поиск назад, начиная
с байта n, на который указывает s, а не вперед — от первого байта. В отличие от
memchr(), memrchr() — это расширение GNU, а не часть языка C.
Более сложные поисковые задачи решаются с помощью функции memmem(). Она
ищет в блоке памяти произвольный массив байтов:
#define _GNU_SOURCE
#include
void * memmem (const void *haystack,
size_t haystacklen,
const void *needle,
size_t needlelen);

Функция memmem() возвращает указатель на первый экземпляр подблока needle,
имеющего длину needlelen байт, который, в свою очередь, находится в блоке памя­
ти haystack длиной haystacklen байт. Если функции не удается найти needle в haystack,
она возвращает NULL. Эта функция также является расширением GNU.

Перещелкивание байтов
Библиотека Linux C предоставляет интерфейс для тривиального сворачивания
байтов данных:
#define _GNU_SOURCE
#include
void * memfrob (void *s, size_t n);

Вызов memfrob() затемняет первые n байт в памяти, начиная с s. При этом при­
меняется исключающее «ИЛИ» (XOR) к каждому байту с числом 42. Вызов воз­
вращает s.
Эффект от вызова memfrob() можно обратить, вновь вызвав memfrob() примени­
тельно к той же области памяти, поэтому следующий код является холостой ко­
мандой, применяемой с secret:
memfrob (memfrob (secret, len), len);

Эта функция ни в коем случае не является полноценной (и даже приемлемой)
заменой шифрования; ее использование ограничено несложным затемнением строк.
Функция является уникальной для GNU.

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

Блокировка памяти

359

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

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

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

Блокировка части адресного пространства
Стандарт POSIX 1003.1b-1993 определяет два интерфейса для «блокировки» одной
или нескольких страниц в физической памяти. Это гарантирует, что данные стра­
ницы ни при каких условиях не будут выгружены на диск. Первая функция бло­
кирует заданный интервал адресов:
#include
int mlock (const void *addr, size_t len);

360

Глава 9. Управление памятью

Вызов mlock() блокирует область виртуальной памяти, начинающуюся с addr
и продолжающуюся в физическую память на len байт. При успехе этот вызов воз­
вращает 0; при ошибке он возвращает –1 и устанавливает errno соответствующее
значение.
Успешный вызов блокирует в памяти все те страницы, которые содержат
[addr,addr+len). Например, если при вызове указан всего один байт, то в памяти
блокируется вся страница, на которой он находится. Стандарт POSIX диктует,
чтобы addr был выровнен по границе страниц. Linux не требует строгого выполне­
ния этого правила, при необходимости бесшумно округляя addr вниз до ближайшей
целой страницы. Однако если в программе необходимо обеспечить переносимость
в другие системы, то требуется гарантировать, что addr будет выровнен по границе
страниц.
Допустимы следующие коды errno:
 EINVAL — параметр len является отрицательным;
 ENOMEM — вызывающая сторона попыталась заблокировать больше страниц, чем
допускает лимит ресурса RLIMIT_MEMLOCK (подробнее см. в подразд. «Лимиты

блокировки» ниже);
 EPERM — лимит ресурса RLIMIT_MEMLOCK был равен 0, но процесс не обладал воз­
можностью CAP_IPC_LOCK (также см. подразд. «Лимиты блокировки» ниже).
ПРИМЕЧАНИЕ
Дочерний процесс не наследует заблокированный статус памяти через fork(). Однако, поскольку
в адресных пространствах Linux действует поведение копирования при записи, страницы дочернего процесса фактически блокируются в памяти, пока этот потомок не запишет в них что-нибудь.

В качестве примера предположим, что программа держит в памяти расшифро­
ванную строку. Процесс может заблокировать страницу, содержащую эту строку,
с помощью кода вроде следующего:
int ret;
/* блокируем 'secret' в памяти */
ret = mlock (secret, strlen (secret));
if (ret)
perror ("mlock");

Блокировка всего адресного пространства
Если процессу требуется удерживать все свое адресное пространство в физической
памяти, то интерфейс mlock() для этого довольно неудобен. В таком случае, который
вполне обычен в приложениях реального времени, POSIX определяет специальный
системный вызов, блокирующий все адресное пространство:
#include
int mlockall (int flags);

Блокировка памяти

361

Вызов mlockall() блокирует все страницы в адресном пространстве актуально­
го процесса, удерживая их в физической памяти. Параметр flags, управляющий
таким поведением и представляющий собой побитовое «ИЛИ», может принимать
одно из следующих двух значений:
 MCL_CURRENT — задав это значение, мы приказываем mlockall() заблокировать

в адресном пространстве процесса все страницы, отображенные в данный момент:
стек, сегмент данных, отображенные файлы и т. д.;
 MCL_FUTURE — указав данное значение, мы заставляем mlockall() гарантировать,

чтобы все страницы, отображаемые в данное адресное пространство в будущем,
также блокировались в памяти.
В большинстве приложений указывается побитовое «ИЛИ» из двух этих зна­
чений.
В случае успеха этот вызов возвращает 0; при ошибке он возвращает –1 и при­
сваивает errno один из следующих кодов ошибки:
 EINVAL — параметр flags является отрицательным;
 ENOMEM — вызывающая сторона попыталась заблокировать больше страниц, чем
допускает лимит ресурса RLIMIT_MEMLOCK (подробнее см. в подразд. «Лимиты

блокировки» ниже);
 EPERM — лимит ресурса RLIMIT_MEMLOCK был равен 0, но процесс не обладал воз­
можностью CAP_IPC_LOCK (также см. подразд. «Лимиты блокировки» ниже).

Разблокировка памяти
Чтобы разблокировать страницы, находящиеся в физической памяти, и вновь
позволить ядру при необходимости выгрузить их на диск, POSIX стандартизиру­
ет еще два интерфейса:
#include
int munlock (const void *addr, size_t len);
int munlockall (void);

Системный вызов munlock() разблокирует страницы, начиная с адреса addr
и вплоть до len байт. Он отменяет действие mlock(). Системный вызов munlockall()
отменяет действие mlockall(). Оба вызова в случае успеха возвращают 0, а при
ошибке возвращают –1, присваивая errno одно из следующих значений:
 EINVAL — параметр len является недопустимым (только для munlock());
 ENOMEM — некоторые из указанных страниц являются недопустимыми;
 EPERM — лимит ресурса RLIMIT_MEMLOCK был равен 0, но процесс не обладал воз­
можностью CAP_IPC_LOCK (см. подразд. «Лимиты блокировки» ниже).

Блокировки памяти не вкладываются друг в друга, поэтому отдельный вызов
mlock() или munlock() высвободит заблокированную страницу независимо от того,
сколько раз она блокировалась с помощью mlock() или mlockall().

362

Глава 9. Управление памятью

Лимиты блокировки
Блокировка памяти может влиять на общую производительность системы. Дей­
ствительно, если будет заблокировано слишком много страниц, то выделения памя­
ти могут пробуксовывать, поскольку Linux лимитирует количество страниц, кото­
рые может заблокировать процесс.
Процессы, обладающие возможностью CAP_IPC_LOCK, могут блокировать в памя­
ти любое количество страниц. Процессы, не имеющие такой характеристики, могут
блокировать всего RLIMIT_MEMLOCK байт. По умолчанию данный лимит равен
32 Кбайт — вполне достаточно, чтобы хранить в памяти один-два секрета, но такой
объем еще не оказывает негативного эффекта на общую производительность. Ли­
миты ресурсов рассматривались в гл. 6, там же было рассказано, как получать
и изменять это значение.

Находится ли страница в физической памяти
Для отладки и диагностики Linux использует функцию mincore(), позволяющую
определять, находится указанный диапазон в физической памяти или выгружен
на диск:
#include
#include
int mincore (void *start,
size_t length,
unsigned char *vec);

Вызов mincore() предоставляет массив, указывающий, какие отображаемые
страницы находятся в физической памяти в момент системного вызова. Вызов
возвращает массив посредством vec и описывает страницы, начиная со start (тре­
буется выравнивание по границам страниц) и до length байт (выравнивание по
границам страниц не нужно). Каждый байт в vec соответствует одной странице
в предоставленном диапазоне. Первый байт описывает первую страницу в этом
диапазоне, второй — вторую и т. д. линейно. Следовательно, vec должен быть до­
статочно велик, чтобы содержать (length – 1 + размер_страницы) / размер_страни­
цы байт. Самый младший бит в каждом байте равен 1, если страница находится
в физической памяти, и 0, если не находится. Остальные биты в настоящее время
остаются неопределенными и зарезервированы для использования в будущем.
В случае успеха вызов возвращает 0. При ошибке он возвращает –1 и присваи­
вает errno одно из следующих значений:
 EAGAIN — недостаточно ресурсов ядра для выполнения данного запроса;
 EFAULT — параметр vec указывает на недействительный адрес;
 EINVAL — значение параметра start не выровнено по границам страниц;
 ENOMEM — в диапазоне [address,address+1) содержится память, не являющаяся

частью файлового отображения.

Уступающее выделение памяти

363

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

Уступающее выделение памяти
Linux использует стратегию уступающего выделения памяти. Когда процесс
требует из ядра дополнительную память — например, при увеличении своего
сегмента данных или создании нового отображения в памяти, — ядро фиксирует
обязательство выделить память, не предоставляя при этом никакого физическо­
го хранилища. Лишь когда процесс записывает информацию в новый выделенный
объем памяти, ядро удовлетворяет это обязательство, предоставляя для обещан­
ного объема памяти физическое пространство. Ядро делает это последовательно
для каждой страницы, по мере необходимости выполняя подкачку страниц по
требованию и копирование при записи.
Такое поведение имеет следующие сильные стороны. Во-первых, подобное
ленивое выделение памяти позволяет ядру откладывать большую часть работы до
самого последнего момента, в который ее необходимо выполнить, — притом что,
возможно, некоторые выделения и не придется удовлетворять. Во-вторых, посколь­
ку запросы удовлетворяются последовательно для каждой страницы, и при этом по
требованию, физическое пространство требуется только для памяти, которая дей­
ствительно используется в настоящий момент. В конечном счете объем обещанной
памяти может значительно превышать количество доступной даже с учетом раз­
деления подкачки. Последняя возможность называется избыточным выделением.
Избыточное выделение и полный расход памяти (OOM). Избыточное выде­
ление памяти позволяет системе одновременно запускать гораздо больше прило­
жений (при этом довольно объемных), чем это было бы реализуемо в ситуации,
когда каждая запрошенная страница памяти должна быть обеспечена физическим
хранилищем уже в момент выделения, а не в момент фактического использования.
При избыточном выделении для отображения в память файла, имеющего размер
2 Гбайт, место в памяти требуется только для страницы, на которую процесс в на­
стоящий момент записывает данные. Аналогично без избыточного выделения
каждый вызов fork() потребовал бы достаточно свободного пространства, чтобы
продублировать все адресное пространство, несмотря на то что большинство стра­
ниц так и не подверглись бы копированию при записи.
Однако что произойдет, если процессы попытаются удовлетворить больше
обязательств, чем позволяет доступный объем физической памяти и область
подкачки в конкретной системе? В таком случае один запрос или более удов­
летворены не будут. Ядро уже «пообещало» выделить память — системный вызов,
запрашивавший память, был успешен, — поэтому ядру остается принудительно
завершить (убить) процесс, чтобы высвободить нужную память.
Когда из-за избыточного выделения возникает недостаток памяти и не удается
удовлетворить запрос, несмотря на данное ранее обязательство, возникает условие

364

Глава 9. Управление памятью

нехватки памяти (OOM). При наступлении такой ситуации ядро задействует
механизм, называемый OOM killer, выбирающий процесс для принудительного
завершения. В данном случае ядро пытается найти наименее важный процесс,
потребляющий при этом максимальное количество памяти.
Ситуация OOM возникает редко. Поэтому если избыточное выделение будет
разрешено, то в первую очередь это принесет вам огромную пользу. Тем не менее
неожиданный дефицит памяти очень нежелателен, а труднопредсказуемое завер­
шение процесса в подобных условиях часто является недопустимым.
В системах, в которых такой ситуации действительно лучше не допускать, ядро
позволяет деактивировать избыточное выделение памяти в файле /proc/sys/vm/
overcommit_memory, а также аналогичный параметр sysctl vm.overcommit_memory.
По умолчанию этот параметр имеет значение 0. При таком значении ядро долж­
но применять эвристическую стратегию избыточного выделения: разумно исполь­
зовать избыточное выделение, но отклонять запросы, которые кажутся вопиющи­
ми. Значение 1 обеспечивает успешное выполнение всех обязательств, при этом
предупреждение пропадает впустую. Некоторые приложения, интенсивно потреб­
ляющие память (в частности, исследовательские инструменты), зачастую запраши­
вают гораздо больше памяти, чем им на самом деле требуется, поэтому такой пара­
метр целесообразен.
Значение 2 полностью отключает избыточное выделение и активирует строгий
учет. В этом случае обязательства по выделению памяти ограничены объемом
раздела подкачки плюс настраиваемый процент физической памяти. Этот процент
задается в файле /proc/sys/vm/overcommit_ratio или с помощью аналогичного пара­
метра sysctl, равного vm.overcommit_ratio. По умолчанию действует значение 50,
ограничивающее обязательства по выделению памяти объемом раздела подкачки
плюс 50 % физической памяти. В физической памяти находится ядро, страницы
подкачки, страницы, зарезервированные в системе, заблокированные страницы
и т. д., поэтому только часть этой памяти действительно доступна для подкачки
и гарантированно может использоваться для удовлетворения обязательств.
Будьте осторожны со строгим учетом! Многие разработчики систем, шараха­
ющиеся от OOM killer, считают строгий учет панацеей. Тем не менее приложения
часто выполняют множество ненужных операций выделения, сильно захватыва­
ющих «избыточную» область. Обеспечение такого поведения — одна из основных
причин, по которым машины оснащаются виртуальной памятью.

СигнаЛЬI

Сигналы

-

это программные прерывания, обеспечивающие асинхронную обработ­

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

-

Ctrl+C.

Другие ис­

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

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

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

(IPC)

один процесс также

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

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

-

например, пользователь может нажать

ты программы,

-

Ctrl+C в

любой момент рабо­

но и обработка сигналов в программе выполняется асинхронно.

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

UNIX

в этой операционной системе применялись

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

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

UNIX вносили

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

POSIX,

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

и используется в

Linux,

о нем мы здесь и поговорим.

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

интерфейсы

Linux, обеспечивающие управление сигналами и манипулирование

ими.

Самые нетривиальные приложения взаимодействуют с использованием сигна­

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

-

-

и это зачастую

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

димы при завершении программы.

366

Глава 10. Сигналы

Концепции, связанные с сигналами
Сигналы обладают очень строгим жизненным циклом. Сначала сигнал порождается
(иногда также говорят, что он отсылается или генерируется). Затем ядро сохраняет
сигнал до тех пор, пока не сможет его доставить. Наконец, как только появляется
такая возможность, ядро обрабатывает сигнал требуемым образом. В зависимости
от требований процесса ядро может выполнить одно из трех действий.
 Игнорировать сигнал. Никаких действий не предпринимается. Есть два сигнала,
которые не могут быть проигнорированы: SIGKILL и SIGSTOP. Дело в том, что

системный администратор должен иметь возможность останавливать или за­
вершать (убивать) процессы. Если бы процесс был способен проигнорировать
SIGKILL (стать «бессмертным») или SIGSTOP (стать «неудержимым»), то систем­
ный администратор лишился бы такой возможности.
 Перехватить сигнал и обработать его. Ядро приостанавливает исполнение

текущего кода в процессе и переключается на функцию, которая была зареги­
стрирована ранее. Затем процесс выполняет эту функцию. Когда он вернется
после ее выполнения, процесс вновь перейдет к выполнению той работы, кото­
рую прервал в момент получения сигнала. Особенно часто приходится отлав­
ливать сигналы SIGINT и SIGTERM. Процессы отлавливают SIGINT, реагируя на
действие пользователя, сгенерировавшего символ прерывания. Например, тер­
минал может перехватить этот сигнал и вернуться в основное окно с приглаше­
нием. Процессы отлавливают SIGTERM для выполнения необходимой очистки,
например для отсоединения от сети или для удаления временных файлов — до
завершения. Сигналы SIGKILL и SIGSTOP нельзя перехватить.
 Выполнение действия, задаваемого по умолчанию. Это действие зависит от того,

какой именно сигнал был отправлен. Зачастую стандартное действие — это
завершение процесса. Например, именно так обрабатывается сигнал SIGKILL.
Тем не менее многие сигналы предоставляются для строго определенной цели
и интересуют программиста лишь в конкретной ситуации. Поэтому по умолча­
нию подобные сигналы игнорируются, так как большинство программ в них «не
заинтересованы». Ниже мы рассмотрим различные сигналы и стандартные
действия, выполняемые по умолчанию при их получении.
В прежние времена, когда сигнал был доставлен, обработавшая его функция не
имела никакой информации о том, что же произошло, — она знала только о самом
факте возникновения определенного сигнала. В настоящее время ядро может пре­
доставлять заинтересованному программисту широкий контекст происходящего.
Как мы вскоре увидим, сигналы могут передавать даже данные, определяемые
пользователем.

Идентификаторы сигналов
У каждого сигнала есть символьное имя, начинающееся с префикса SIG. Например,
SIGINT отсылается, если пользователь нажал Ctrl+C. Сигнал SIGABRT генерируется,

367

Концепции, связанные с сигналами

когда процесс вызывает функцию abort(). Наконец, сигнал SIGKILL посылается при
принудительном завершении процесса.
Все эти сигналы описываются в заголовочном файле, включаемом из .
Сигналы — это просто препроцессорные определения, представляющие положитель­
ные целые числа. Таким образом, каждый сигнал ассоциирован с целочисленным
идентификатором. У сигналов отображение имени на целое число зависит от реали­
зации и отличается в разных системах UNIX. Правда, около десятка наиболее рас­
пространенных сигналов везде отображаются одинаково (например, SIGKILL — это
печально известный сигнал 9). В портируемой программе каждый сигнал всегда
должен иметь имя, удобное для чтения, и никогда — целочисленное значение.
Номера сигналов начинаются с 1 (обычно это SIGHUP) и далее возрастают. Всего
существует около 30 сигналов, но большинство программ регулярно используют
лишь несколько из них. Сигнала со значением 0 не существует; это специальное
значение, известное как нулевой сигнал. С нулевым сигналом не связано решитель­
но ничего интересного — он не заслуживает собственного имени. Но некоторые
системные вызовы (например, kill()) используют значение 0 в качестве специаль­
ного случая.
ПРИМЕЧАНИЕ
Чтобы вывести список сигналов, поддерживаемых в вашей системе, выполните команду kill –l.

Сигналы, поддерживаемые в Linux
В табл. 10.1 перечислены сигналы, поддерживаемые в Linux.
Таблица 10.1. Сигналы1
Сигнал
SIGABRT
SIGALRM
SIGBUS
SIGCHLD
SIGCONT
SIGFPE
SIGHUP

SIGILL
SIGINT
SIGIO
SIGKILL

Описание
Отправляется функцией abort()
Отправляется функцией alarm()
Аппаратная ошибка или ошибка выравнивания
Дочерний процесс был завершен
Процесс продолжил работу и был остановлен
Арифметическое исключение
Терминал, управляющий процессом, был закрыт
(как правило, при выходе пользователя
из системы)
Процесс попытался выполнить недопустимую
инструкцию
Пользователь сгенерировал символ прерывания
(Ctrl+C)
Событие асинхронного ввода/вывода
Неотлавливаемое завершение процесса

Действие по умолчанию
Завершение с дампом ядра
Завершение
Завершение с дампом ядра
Игнорируется
Игнорируется
Завершение с дампом ядра
Завершение

Завершение с дампом ядра
Завершение
Завершение1
Завершение
Продолжение 

1

В других UNIX-системах, например в BSD, этот сигнал игнорируется.

368

Глава 10. Сигналы

Таблица 10.1 (продолжение)
Сигнал
SIGPIPE
SIGPROF
SIGPWR
SIGQUIT
SIGSEGV
SIGSTKFLT
SIGSTOP
SIGSYS
SIGTERM
SIGTRAP
SIGTSTP
SIGTTIN
SIGTTOU
SIGURG
SIGUSR1
SIGUSR2
SIGVTALRM
SIGWINCH
SIGXCPU
SIGXFSZ

Описание
Процесс записал информацию в конвейер,
но считывателей не было
Истекло время, заданное на профилирующем
таймере
Сбой в энергоснабжении
От пользователя получен символ выхода (Ctrl+\)
Нарушение доступа к памяти
Ошибка в стеке сопроцессора
Приостанавливает выполнение процесса
Процесс попытался выполнить недопустимый
системный вызов
Отлавливаемое завершение процесса
Достигнута точка останова
Пользователь сгенерировал символ приостановки
(Ctrl+Z)
Фоновый процесс выполнил считывание с управляющего терминала
Фоновый процесс выполнил запись в управля­
ющий терминал
Операция ввода/вывода срочно требует обработки
Сигнал, определяемый процессом
Сигнал, определяемый процессом
Генерируется функцией setitimer() при вызове
с флагом ITIMER_VIRTUAL
Изменился размер окна управляющего терминала
Превышен лимит ресурсов, выделенных процессу
Превышен лимит ресурсов, выделенных файлу

Действие по умолчанию
Завершение
Завершение
Завершение
Завершение с дампом ядра
Завершение с дампом ядра
Завершение1
Остановка
Завершение с дампом ядра
Завершение
Завершение с дампом ядра
Остановка
Остановка
Остановка
Игнорируется
Завершение
Завершение
Завершение
Игнорируется
Завершение с дампом ядра
Завершение с дампом ядра

Существует еще несколько значений сигналов, но в Linux они считаются экви­
валентными другим значениям. SIGINFO определяется как SIGPWR2, SIGIOT — как
SIGABRT, а SIGPOLL и SIGLOST — как SIGIO.
Итак, для справки у нас есть вышеприведенная таблица, так что подробно рас­
смотрим все перечисленные в ней сигналы.
1

 SIGABRT — функция abort() отсылает этот сигнал процессу, который ее вызвал.
После этого процесс завершается и генерирует файл ядра. В Linux abort() воз­
никает, когда контрольные утверждения, например assert(), не выполняются.
 SIGALRM — функции alarm() и setitimer() (вторая — при флаге ITIMER_REAL) отсы­

лают этот сигнал вызвавшему их процессу, когда завершается тревожный пе­
риод. Эти функции и другие, похожие на них, рассмотрены в гл. 11.
1

2

Ядро Linux больше не генерирует этот сигнал; он сохраняется только для обеспечения
обратной совместимости.
Этот сигнал определяется только в архитектуре Alpha. Во всех других машинных архи­
тектурах этот сигнал не существует.

Концепции, связанные с сигналами

369

 SIGBUS — ядро порождает этот сигнал, когда процесс вызывает любую ошибку

оборудования, не связанную с защитой памяти (в последнем случае генери­
руется SIGSEGV). В традиционных системах UNIX этот сигнал представляет
различные неисправимые ошибки, например невыровненное обращение к па­
мяти. Но в ядре Linux большинство подобных ошибок исправляются автома­
тически, без генерирования сигнала. Ядро порождает такой сигнал лишь в том
случае, когда процесс ненадлежащим образом обращается к области памяти,
созданной с помощью mmap() (мы подробно обсудили отображения в памяти
в гл. 9). Если этот сигнал не перехвачен, то ядро завершит процесс и выполнит
дамп ядра.
 SIGCHLD — каждый раз при завершении или остановке процесса ядро посылает
этот сигнал его процессу-родителю. Поскольку по умолчанию SIGCHLD игнори­

руется, процессы должны явно отлавливать и обрабатывать его, если их инте­
ресует «жизнь собственных потомков». Обработчик этого сигнала обычно вы­
зывает функцию wait() (см. гл. 5), чтобы узнать идентификатор pid процесса
и код выхода.
 SIGCONT — ядро посылает этот сигнал процессу, когда процесс должен возобновить

работу после остановки. По умолчанию этот сигнал игнорируется, но процессы
могут отлавливать его, если «планируют» совершить какое-то действие, возвра­
щаясь к работе. Данный сигнал обычно используется на терминалах или в ре­
дакторах, которые при возвращении к работе обновляют экран.
 SIGFPE — несмотря на название этот сигнал представляет любое арифметическое

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

терминала. Ядро также отправляет этот сигнал каждому из тех процессов, ко­
торые работают в приоритетном режиме, когда завершается ведущий процесс
сеанса. В данном случае по умолчанию применяется завершение, и это логич­
но — ведь сигнал означает, что пользователь вышел из системы. Служебные
процессы (демоны) перегружают этот сигнал, обогащая его механизмом, тре­
бующим перезагрузить конфигурационные файлы. Например, посылая SIGHUP
серверу Apache, мы приказываем ему заново прочитать httpd.conf. Использова­
ние SIGHUP для этой цели — распространенное соглашение, но такой метод не­
обязателен. Эта практика безопасна, поскольку у демонов нетуправляющих
терминалов, поэтому они, как правило, не должны получать такой сигнал.
 SIGILL — ядро генерирует этот сигнал, когда процесс пытается выполнить недо­

пустимую машинную инструкцию. Стандартное действие — завершить процесс
и выполнить дамп ядра. Процессы могут по своему усмотрению отлавливать

370

Глава 10. Сигналы

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

режиме, когда пользователь вводит символ прерывания (обычно Ctrl+C). Стан­
дартное действие — завершение процесса, но по своему усмотрению процессы
могут отлавливать и обрабатывать этот сигнал. Обычно они так и делают, чтобы
выполнить очистку перед завершением.
 SIGIO — отправляется, когда генерируется событие асинхронного ввода/выво­

да в стиле BSD. Подобный ввод/вывод в Linux используется редко. Распро­
страненные в Linux приемы расширенного ввода/вывода подробно рассмотре­
ны в гл. 4.
 SIGKILL — порождается системным вызовом kill(). Он предоставляет системным

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

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

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

когда наступает низкий уровень заряда батареи (например, при работе с блоком
бесперебойного питания UPS). Демон, выполняющий мониторинг UPS, посы­
лает этот сигнал процессу init, который в ответ осуществляет очистку и завер­
шает работу системы — остается надеяться, еще до того, как питание оконча­
тельно иссякнет!
 SIGQUIT — ядро генерирует этот сигнал для всех процессов из группы, работа­

ющей в приоритетном режиме, когда пользователь вводит символ завершения
сеанса на терминале (обычно Ctrl+\). По умолчанию все процессы завершаются,
и генерируется дамп ядра.
 SIGSEGV — этот сигнал, название которого является сокращением от выражения

segmentation violation («нарушение сегментирования»), передается процессу,
когда тот пытается совершить недопустимое обращение к памяти. В частности,
к таким ситуациям относится попытка обращения к неотображенной памяти,
попытка считывания из памяти, которая закрыта для чтения, выполнение кода
в области памяти, не допускающей выполнения, либо запись в область памяти,
не предназначенную для записи. Процессы могут перехватывать и обрабатывать
этот сигнал, но по умолчанию процесс завершается и генерируется дамп ядра.
 SIGSTOP — может быть отправлен только системным вызовом kill(). Он без­

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

Концепции, связанные с сигналами

371

 SIGSYS — ядро отправляет этот сигнал процессу, который пытается сделать не­

допустимый системный вызов. Это может произойти, если двоичный файл был
создан в более новой версии операционной системы, а используется в более
старой. Правильно построенные двоичные файлы, выполняющие свои систем­
ные вызовы через glibc, не получают такого сигнала. Вместо этого недопустимые
системные вызовы должны возвращать –1 и присваивать переменной errno зна­
чение ENOSYS.
 SIGTERM — сигнал генерируется только системным вызовом kill() и позволяет

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

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

в приоритетном режиме, когда пользователь вводит символ приостановки
(обычно Ctrl+Z).
 SIGTTIN — направляется к процессу, работающему в фоновом режиме, когда он

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

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

полосные данные (в этой книге они не рассматриваются).
 SIGUSR1 и SIGUSR2 — доступны для выполнения задач, определяемых пользовате­
лем. Ядро никогда их не посылает. Процессы могут использовать SIGUSR1 и SIGUSR2

для любых целей по своему усмотрению. Как правило, с их помощью даются
инструкции процессу-демону, чтобы он действовал не так, как обычно. По умол­
чанию при получении любого из этих сигналов процесс завершается.
 SIGVTALRM — функция setitimer() отсылает этот сигнал, когда истекает время на
таймере, созданном с применением флага ITIMER_VIRTUAL. Таймеры будут рас­

смотрены в гл. 11.
 SIGWINCH — ядро отправляет этот сигнал всем процессам из группы, работающей

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

372

Глава 10. Сигналы

 SIGXCPU — ядро порождает этот сигнал, когда процесс превышает отведенный ему

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

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

Основы управления сигналами
Итак, мы разобрались со значениями всех сигналов. Теперь рассмотрим, как управ­
лять ими внутри вашей программы. Простейший и самый старый интерфейс для
работы с сигналами — это функция signal(). Она определяется в стандарте ISO C89,
который регламентирует лишь наименьший «общий знаменатель» поддержки сиг­
налов. Поэтому данный системный вызов очень прост. Linux предоставляет и другие
интерфейсы, обеспечивающие гораздо более полный контроль при работе с сигна­
лами. Эти функции будут рассмотрены ниже в текущей главе. Поскольку функция
signal() — самая простая из себе подобных, а также описана в ISO C и потому до­
вольно распространена, мы для начала рассмотрим именно ее:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal (int signo, sighandler_t handler);

Успешный вызов signal() отменяет актуальное действие, предпринятое в ответ
на получение сигнала signo. Вместо этого сигнал обрабатывается с помощью меха­
низма, указанного в handler. signo — это название одного из сигналов, рассмотрен­
ных в предыдущем разделе, например SIGINT или SIGUSR1. Как вы помните, процесс
не сможет перехватить сигналы SIGKILL и SIGSTOP, поэтому устанавливать один из
них в качестве значения для обработчика не имеет смысла.
Функция handler должна возвращать void. И это правильно, поскольку для этой
функции (в отличие от обычных) в программе не предусмотрено стандартное место
для возврата. Эта функция принимает один аргумент — целое число, являющееся
идентификатором обрабатываемого сигнала (например, SIGUSR2). Он позволяет
одной функции обрабатывать сразу много сигналов. Прототип имеет следующую
форму:
void my_handler (int signo);

Для определения этого прототипа Linux использует объявление sighandler_t.
В других системах UNIX напрямую задействуются указатели функций. В отдельных

Основы управления сигналами

373

системах вы найдете собственные типы, которые могут называться не sighandler_t,
а как-то иначе. Если необходимо обеспечить переносимость программы, то в ней не
следует напрямую ссылаться на типы.
Когда эта функция направляет сигнал к процессу, зарегистрировавшему об­
работчик сигналов, ядро приостанавливает выполнение основного потока ин­
струкций программы и вызывает обработчик сигнала. Обработчик получает
значение сигнала, которое представляет собой signo , изначально переданное
функции signal().
Вы также можете применять функцию signal(), чтобы приказать ядру проигно­
рировать указанный сигнал для актуального процесса либо чтобы сбросить сигнал
и применить поведение, заданное по умолчанию. Это делается с помощью специ­
альных значений, присваиваемых параметру handler:
 SIG_DFL — устанавливает поведение сигнала, указанного как signo, в качестве
действующего по умолчанию. Например, в случае SIGPIPE процесс будет за­

вершен;
 SIG_IGN — игнорирует сигнал, указанный как signo.

Функция signal() возвращает предыдущее поведение сигнала. Это может быть
применение указателя на обработчик сигнала, значение SIG_DFL или SIG_IGN. При ошиб­
ке функция возвращает SIG_ERR. Она не устанавливает значения errno.

Ожидание любого сигнала
Стандарт POSIX определяет системный вызов pause(), полезный при отладке и при
написании демонстрационных фрагментов кода. Этот вызов переводит процесс
в спящий режим до тех пор, пока процесс не получит сигнал, который либо будет
обработан, либо завершит сам процесс:
#include
int pause (void);

Функция pause() возвращается лишь в том случае, если сигнал получен (и, со­
ответственно, этот сигнал обрабатывается). В такой ситуации pause() возвращает
–1 и присваивает errno значение EINTR. Если ядро порождает игнорируемый данным
процессом сигнал, то процесс не просыпается.
В ядре Linux pause() является одним из простейших системных вызовов. Он
выполняет всего два действия. Во-первых, переводит процесс в непрерывный спя­
щий режим. Во-вторых, вызывает schedule(), чтобы активизировать планировщик
процессов Linux и найти другой процесс для запуска. Поскольку этот процесс
фактически ничего не ожидает, ядро не станет его «будить», пока он не получит
сигнал. Все эти перипетии выражаются всего в двух строках кода на C1.
1

Следовательно, pause() не является простейшим из существующих системных вызо­
вов. Вызовы getpid() и gettid() еще проще — каждый из них занимает всего одну
строку.

374

Глава 10. Сигналы

Примеры
Рассмотрим пару простых примеров. В первом из них мы регистрируем обработчик
сигнала SIGINT, который просто выводит сообщение, а затем завершает программу
(как обычно при использовании SIGINT):
#include
#include
#include
#include






/* обработчик для SIGINT */
static void sigint_handler (int signo)
{
/*
* Технически не следует использовать printf()
* в обработчике сигнала, но если вы так сделаете, это не смертельно.
* Подробнее об этом мы поговорим в разделе
* «Реентерабельность».
*/
printf ("Захвачен сигнал SIGINT!\n");
exit (EXIT_SUCCESS);
}
int main (void)
{
/*
* Регистрируем sigint_handler как наш обработчик
* для сигнала SIGINT.
*/
if (signal (SIGINT, sigint_handler) == SIG_ERR) {
fprintf (stderr, "Невозможно обработать SIGINT!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause ();
}

return 0;

В следующем примере мы зарегистрируем точно такой же обработчик для SIGTERM
и SIGINT. Кроме того, мы сбросим поведение сигнала SIGPROF, чтобы он действовал,
как установлено по умолчанию (процесс будет завершаться), и проигнорируем
SIGHUP (который в противном случае завершил бы процесс):
#include
#include
#include
#include






/* обработчик для SIGINT и SIGTERM */

Основы управления сигналами

static void signal_handler (int signo)
{
if (signo == SIGINT)
printf ("Захвачен сигнал SIGINT!\n");
else if (signo == SIGTERM)
printf ("Захвачен сигнал SIGTERM!\n");
else {
/* этого ни в коем случае не должно произойти */
fprintf (stderr, "Неожиданный сигнал!\n");
exit (EXIT_FAILURE);
}
exit (EXIT_SUCCESS);
}
int main (void)
{
/*
* Регистрируем signal_handler как наш обработчик сигнала
* для SIGINT.
*/
if (signal (SIGINT, signal_handler) == SIG_ERR) {
fprintf (stderr, "Невозможно обработать SIGINT!\n");
exit (EXIT_FAILURE);
}
/*
* Регистрируем signal_handler как наш обработчик сигнала
* для SIGTERM.
*/
if (signal (SIGTERM, signal_handler) == SIG_ERR) {
fprintf (stderr, "Невозможно обработать SIGTERM!\n");
exit (EXIT_FAILURE);
}
/* Сбрасываем поведение SIGPROF, чтобы он действовал как по умолчанию. */
if (signal (SIGPROF, SIG_DFL) == SIG_ERR) {
fprintf (stderr, "Невозможно сбросить SIGPROF!\n");
exit (EXIT_FAILURE);
}
/* Игнорируем SIGHUP. */
if (signal (SIGHUP, SIG_IGN) == SIG_ERR) {
fprintf (stderr, "Невозможно игнорировать SIGHUP!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause ();
return 0;
}

375

376

Глава 10. Сигналы

Выполнение и наследование
При ветвлении дочерний процесс наследует все сигнальные действия от своего
родительского процесса. Это означает, что процесс-потомок копирует у родителя
зарегистрированные действия (игнорировать, применить действие по умолчанию,
обработать) для каждого из сигналов. Сигналы, ожидающие обработки, не наследуются, и это правильно: такой сигнал будет отправлен процессу с конкретным
pid, а определенно не его потомку.
Когда процесс создается с помощью одного из системных вызовов, относящих­
ся к семейству exec, все сигналы действуют так, как это задано по умолчанию, если
только родительский процесс их не игнорирует. В последнем случае новый отобра­
женный процесс также будет их игнорировать. Иными словами: любой сигнал,
перехваченный процессом-предком до exec, после exec выполняет действие, опре­
деленное по умолчанию, а все остальные сигналы остаются без изменений. Это
действительно целесообразно, поскольку свежезапущенный процесс не использу­
ет совместно с предком его адресное пространство. Следовательно, могут отсут­
ствовать какие-либо зарегистрированные обработчики сигналов. Сигналы, ожида­
ющие обработки, наследуются. Информация о наследовании обобщена в табл. 10.2.
Таблица 10.2. Наследуемое поведение сигналов
Поведение сигнала
Игнорирование
По умолчанию
Обработка
Ожидающие сигналы

Между ветвлениями
Наследуется
Наследуется
Наследуется
Не наследуется

Между вызовами exec
Наследуется
Наследуется
Не наследуется
Наследуется

Эти варианты поведения при выполнении процесса на практике имеют инте­
ресное практическое преимущество: когда оболочка выполняет процесс в фоновом
режиме (или когда фоновый процесс выполняет другой процесс), такой новый
процесс должен игнорировать символы прерывания и выхода. Следовательно,
прежде чем оболочка запустит фоновый процесс, она должна задать для сигналов
SIGINT и SIGQUIT флаг SIG_IGN. Поэтому зачастую в программах, обрабатывающих
эти сигналы, сначала требуется удостовериться, что они не игнорируются. На­
пример:
/* обрабатываем SIGINT, но только если он не игнорируется */
if (signal (SIGINT, SIG_IGN) != SIG_IGN) {
if (signal (SIGINT, sigint_handler) == SIG_ERR)
fprintf (stderr, "Невозможно обработать SIGINT!\n");
}
/* обрабатываем SIGQUIT, но только если он не игнорируется */
if (signal (SIGQUIT, SIG_IGN) != SIG_IGN) {
if (signal (SIGQUIT, sigquit_handler) == SIG_ERR)
fprintf (stderr, "Невозможно обработать SIGQUIT!\n");
}

Основы управления сигналами

377

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

Сопоставление номеров сигналов и строк
Во всех примерах, рассмотренных до сих пор, мы жестко задавали имена сигналов.
Но иногда бывает более удобно (или даже необходимо) иметь возможность преоб­
разовать номер сигнала в строковое представление его имени. Это можно сделать
несколькими способами. Например, мы можем получить строку из статически
определенного списка:
extern const char * const sys_siglist[];
sys_siglist — это массив строк, содержащий имена сигналов, поддерживаемых
в системе. Они индексируются в массиве номерами сигналов.
Существует альтернативный интерфейс, который определен в BSD и называется
psignal(). Эта функция достаточно распространена, она поддерживается и в Linux.
#include
void psignal (int signo, const char *msg);

Вызов psignal() выводит в stderr ту строку, которую вы предоставили в каче­
стве аргумента msg. Далее следует двоеточие, пробел и сигнал, указанный в signo. Если
msg пропустить, то в stderr будет выводиться только имя сигнала. Если значение
signo недопустимо, то это будет указано в сообщении, которое вы увидите в stderr.
Более удобный интерфейс — функция strsignal(). Она не стандартизирована,
но поддерживается и в Linux, и во многих других системах:
#define _GNU_SOURCE
#include
char * strsignal (int signo);

Вызов strsignal() возвращает указатель на описание сигнала, заданного как
signo. Если значение signo является недопустимым, то это обычно четко описыва­
ется в возвращаемом сообщении (некоторые другие системы UNIX, поддержива­
ющие эту функцию, вместо подобного сообщения возвращают значение NULL).
Возвращаемая строка остается валидной только до следующего вызова strsignal(),
поэтому такая функция небезопасна для потоков.
Как правило, удобнее всего работать с sys_siglist. Воспользовавшись таким
подходом, мы можем следующим образом переписать рассмотренный выше обра­
ботчик сигнала:
static void signal_handler (int signo)
{
printf ("Захвачен сигнал %s\n", sys_siglist[signo]);
}

378

Глава 10. Сигналы

Отправка сигнала
Системный вызов kill(), лежащий в основе распространенной утилиты kill, от­
правляет сигнал от одного процесса к другому:
#include
#include
int kill (pid_t pid, int signo);

В обычной ситуации (когда pid больше нуля) kill() посылает сигнал signo про­
цессу с идентификатором pid.
Если параметр pid равен нулю, signo отправляется всем процессам, относящим­
ся к той же группе, что и вызывающий процесс.
Если pid равен –1, то сигнал signo отсылается всем тем процессам, которым
вызывающий процесс может отправить сигнал (имеет на это право доступа), за
исключением init и себя самого. В следующем подразделе мы обсудим права до­
ступа, управляющие доставкой сигналов.
Если pid меньше –1, то сигнал отсылается группе процессов -pid.
В случае успеха kill() возвращает 0. Вызов считается успешным, как только
удастся послать один сигнал. При ошибке (ни один сигнал послать не удалось)
вызов возвращает –1 и присваивает errno одно из следующих значений:
 EINVAL — сигнал, обозначенный signo, является недопустимым;
 EPERM — вызывающий процесс не обладает достаточными правами доступа, что­

бы послать сигнал какому-либо запрошенному процессу;
 ESRCH — процесс или группа процессов, определенные pid, не существуют либо

(в случае процесса) это процесс-зомби.

Права доступа
Для того чтобы отправить сигнал другому процессу, отсылающий процесс дол­
жен располагать соответствующими правами доступа. Процесс, обладающий
возможностью CAP_KILL (обычно таким процессом владеет администратор),
может послать сигнал любому процессу. Без такой возможности фактический
или реальный пользовательский ID процесса-отправителя должен быть равен
реальному или сохраненному пользовательскому ID процесса-получателя. Проще
говоря, обычный пользователь может послать сигнал лишь такому процессу,
каким владеет сам.
ПРИМЕЧАНИЕ
В UNIX-системах и в Linux в частности определяется исключение для SIGCONT: процесс может
послать этот сигнал любому процессу из того же сеанса. Совпадения пользовательского ID не
требуется.

Отправка сигнала

379

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

Примеры
Далее показан пример отправки сигнала SIGHUP процессу с ID 1722:
int ret;
ret = kill (1722, SIGHUP);
if (ret)
perror ("kill");

Фактически этот фрагмент кода аналогичен следующему, вызывающему ути­
литу kill:
$ kill -HUP 1722

Чтобы проверить, есть ли у нас право отправить сигнал процессу 1722, не по­
сылая при этом сам сигнал, можно поступить так:
int ret;
ret = kill (1722, 0);
if (ret)
; /* право доступа отсутствует */
else
; /* право доступа имеется */

Отправка сигнала самому себе
С помощью функции raise() процесс легко может отправить сигнал сам себе:
#include
int raise (int signo);

Этот вызов:
raise (signo);

эквивалентен следующему:
kill (getpid (), signo);

В случае успеха этот вызов возвращает 0, а при ошибке — ненулевое значение.
Он не устанавливает переменную errno.

380

Глава 10. Сигналы

Отправка сигнала целой группе процессов
Еще одна вспомогательная функция позволяет с удобством отправлять сигнал всем
процессам, относящимся к заданной группе, в тех случаях, когда отрицание ID
группы процессов и использование kill() представляется слишком затратной
операцией:
#include
int killpg (int pgrp, int signo);

Этот вызов
killpg (pgrp, signo);

эквивалентен следующему:
kill (-pgrp, signo);

Вызов остается верным, даже если pgrp равно нулю. В таком случае killpg()
посылает сигнал signo каждому процессу из той группы, к которой относится вы­
зывающий процесс.
При успехе killpg() возвращает 0. При ошибке он возвращает –1 и присваивает
errno одно из следующих значений:
 EINVAL — сигнал, указанный как signo, является недопустимым;
 EPERM — вызывающий процесс не обладает достаточными правами доступа, что­

бы отправить сигнал всем требуемым процессам;
 ESRCH — группа процессов, обозначенная pgrp, не существует.

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

Подробнее об этой возможности см. http://ru.wikipedia.org/wiki/Реентерабельность.

381

Реентерабельность

тельно, было бы неплохо, если бы обработчик сигналов вообще не касался гло­
бальных данных. Но в следующем разделе мы рассмотрим способ временного
блокирования доставки сигналов — так, чтобы можно было безопасно манипули­
ровать данными, которые находятся в совместном использовании между обра­
ботчиком сигналов и остальным процессом.
А что же насчет системных вызовов и других библиотечных функций? Что, если
ваш процесс как раз записывает информацию в файл или выделяет память, а обра­
ботчик сигнала пытается записать данные в тот же файл или пробует вызвать
malloc()? Что делать, если процесс сейчас вызывает функцию, использующую
статический буфер, например strsignal(), а тут приходит сигнал?
Некоторые функции, разумеется, не допускают реентерабельности. Если про­
грамма как раз выполняет нереентерабельную функцию и возникает сигнал, после
которого обработчик сигнала вызывает ту же самую функцию, может возникнуть
хаос. Реентерабельной называется такая функция, которую можно безопасно вы­
зывать из нее же (либо параллельно, из другого потока в том же процессе). Чтобы
функция считалась реентерабельной, она не должна манипулировать статически­
ми данными, а должна иметь дело только с данными, выделенными в стеке и пре­
доставляемыми вызывающей стороной. Кроме того, она не должна вызывать ни­
какую нереентерабельную функцию.
Гарантированно реентерабельные функции. При написании обработчика сиг­
налов вы должны учитывать, что прерываемый процесс может быть остановлен
в ходе выполнения нереентерабельной функции (или любой другой, если уж на то
пошло). Таким образом, обработчики сигналов должны оперировать только реен­
терабельными функциями.
В разных стандартах подготовлены списки функций, которые являются сигналобезопасными: реентерабельными и, соответственно, допустимыми для примене­
ния в обработчике сигналов. Наиболее примечательно, что в POSIX.1-2003 и Single
UNIX Specification дается список функций, которые гарантированно являются
реентерабельными и сигналобезопасными на всех платформах, соответствующих
данному стандарту или спецификации. Эти функции перечислены в табл. 10.3.
Таблица 10.3. Функции, которые гарантированно реентерабельны и безопасны
для работы с сигналами
abort()

accept()

access()

aio_error()

aio_return()

aio_suspend()

alarm()

bind()

cfgetispeed()

cfgetospeed()

cfsetispeed()

cfsetospeed()

chdir()

chmod()

chown()

clock_gettime()

close()

connect()

creat()

dup()

dup2()

execle()

execve()

_Exit()

_exit()

fchmod()

fchown()

fcntl()

fdatasync()

fork()
Продолжение 

382

Глава 10. Сигналы

Таблица 10.3 (продолжение)
fpathconf()
ftruncate()
getgid()
getpgrp()
getsockname()
kill()
lseek()
mkfifo()
pause()
posix_trace_event()
read()
recvfrom()
rmdir()
send()
setgid()
setsockopt()
sigaction()
sigemptyset()
signal()
sigprocmask()
sigsuspend()
socketpair()
sysconf()
tcflush()
tcsendbreak()
time()
timer_settime()
uname()
wait()

fstat()
getegid()
getgroups()
getpid()
getsockopt()
link()
lstat()
open()
pipe()
pselect()
readlink()
recvmsg()
select()
sendmsg()
setpgid()
setuid()
sigaddset()
sigfillset()
sigpause()
sigqueue()
sleep()
stat()
tcdrain()
tcgetattr()
tcsetattr()
timer_getoverrun()
times()
unlink()
waitpid()

fsync()
geteuid()
getpeername()
getppid()
getuid()
listen()
mkdir()
pathconf()
poll()
raise()
recv()
rename()
sem_post()
sendto()
setsid()
shutdown()
sigdelset()
sigismember()
sigpending()
sigset()
socket()
symlink()
tcflow()
tcgetpgrp()
tcsetpgrp()
timer_gettime()
umask()
utime()
write()

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

Наборы сигналов
Отдельным функциям, которые мы рассмотрим ниже в этой главе, требуется ма­
нипулировать наборами сигналов. Например, процесс может блокировать набор
сигналов либо набор сигналов может ожидать обработки. Для этого применяются
операции с наборами сигналов:
#include
int sigemptyset (sigset_t *set);

Наборы сигналов

383

int sigfillset (sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset (sigset_t *set, int signo);
int sigismember (const sigset_t *set, int signo);

Функция sigemptyset() инициализирует набор сигналов, указанный в set, поме­
чая его как пустое множество (из этого набора исключаются все сигналы). Функция
sigfillset() инициализирует набор сигналов, указанный в set, помечая его как
полный (все сигналы включаются в набор). Обе функции возвращают 0. Эти две
функции нужно применить к набору сигналов, прежде чем приступать к дальней­
шему его использованию.
Функция sigaddset() добавляет сигнал signo в набор, указанный в set, а sigdelset()
удаляет signo из того набора сигналов, который указан в set. Обе функции возвра­
щают 0 в случае успеха или –1 при ошибке. Во втором случае переменной errno
присваивается код ошибки EINVAL, и это указывает, что signo является недопустимым
идентификатором сигнала.
Функция sigismember() возвращает 1, если signo — это набор сигналов, указан­
ный в set, 0 — если это другой набор, и –1 — при ошибке. В последнем случае пе­
ременной errno присваивается код EINVAL, указывая, что значение signo является
недопустимым.
Дополнительные функции для работы с сигналами. Все вышеописанные функ­
ции для работы с сигналами соответствуют стандарту POSIX и имеются в любой
современной системе UNIX. Linux также предоставляет несколько нестандартных
функций:
#define _GNU_SOURCE
#define
int sigisemptyset (sigset_t *set);
int sigorset (sigset_t *dest, sigset_t *left, sigset_t *right);
int sigandset (sigset_t *dest, sigset_t *left, sigset_t *right);

Функция sigisemptyset() возвращает 1, если набору сигналов set соответствует
пустое множество, и 0 — в противном случае.
Функция sigorset() помещает объединение (двоичное «ИЛИ») наборов сигна­
лов left и right в dest.
Функция sigandset() помещает пересечение (двоичное «И») наборов сигналов
left и right в dest. Обе функции возвращают 0 в случае успеха и –1 при ошибке,
присваивая errno значение EINVAL.
Эти функции полезны, но ими не следует пользоваться в программах, которые
должны соответствовать POSIX.

384

Глава 10. Сигналы

Блокировка сигналов
Выше мы обсудили реентерабельность и проблемы, возникающие при обращении
с такими обработчиками сигналов, которые могут работать асинхронно, в любое
время. Мы обсудили такие функции, которые нельзя вызывать внутри обработчи­
ка сигнала, так как они являются нереентерабельными.
Но что делать, если вашей программе требуется разделять данные, чтобы они со­
вместно использовались обработчиком сигнала и какой-то другой сущностью в коде?
Что, если на определенных этапах выполнения программы вы не хотите допускать
никаких прерываний, в том числе провоцируемых обработчиками сигналов? Такие
области в программе называются критическими регионами. Мы защищаем их, вре­
менно приостанавливая доставку сигналов, пока программа идет через критический
регион. Принято говорить, что в это время сигналы блокируются. Все сигналы, кото­
рые были порождены в период блокировки, не обрабатываются до тех пор, пока не
наступит разблокировка. Процесс может заблокировать любое количество сигналов.
Набор сигналов, блокированный процессом, называется его сигнальной маской.
POSIX определяет, а Linux реализует специальную функцию для управления
сигнальной маской процесса:
#include
int sigprocmask (int how,
const sigset_t *set,
sigset_t *oldset);

Поведение sigprocmask() зависит от значения how, в качестве которого может
служить один из следующих флагов:
 SIG_SETMASK — сигнальная маска вызывающего процесса изменяется на set;
 SIG_BLOCK — сигналы set добавляются к сигнальной маске вызывающего процес­

са. Иными словами, сигнальная маска заменяется объединением (двоичным
«ИЛИ») из актуальной маски и set;
 SIG_UNBLOCK — сигналы set удаляются из сигнальной маски вызывающего про­
цесса. Иными словами, сигнал заменяется пересечением (двоичным «НЕ») set.

Не допускается разблокировка сигнала, который ранее не был заблокирован.
Если oldset не равно NULL, то функция помещает предыдущий набор сигналов
в oldset.
Если set равно NULL, то функция игнорирует how и не меняет сигнальную маску,
а помещает ее в oldset. Иными словами, передавая в качестве set нулевое значение,
мы можем получить актуальную сигнальную маску.
При успехе вызов возвращает 0. При ошибке он возвращает –1 и присваивает
errno либо EINVAL (это говорит о том, что значение how является недопустимым),
либо EFAULT (если set или oldset является недопустимым указателем).
Блокирование SIGKILL или SIGSTOP не разрешается. Функция sigprocmask() бес­
шумно игнорирует любые попытки добавить сигнал к сигнальной маске.

Расширенное управление сигналами

385

Получение сигналов, ожидающих обработки
Когда ядро генерирует заблокированный сигнал, он не доставляется. Такие сигна­
лы называются ожидающими. Когда ожидающий сигнал разблокируется, ядро
передаст его процессу для обработки.
POSIX определяет функцию для получения набора ожидающих сигналов:
#include
int sigpending (sigset_t *set);

Успешный вызов функции sigpending() помещает набор ожидающих сигналов
в set и возвращает 0. При ошибке вызов возвращает –1 и присваивает errno значение
EFAULT. Это говорит о том, что set является недействительным указателем.

Ожидание набора сигналов
Третья функция, определяемая в POSIX, позволяет процессу временно изменить
его сигнальную маску, а затем дождаться, пока будет сгенерирован новый сигнал,
такой, который будет обработан процессом либо завершит этот процесс.
#include
int sigsuspend (const sigset_t *set);

Если сигнал завершает процесс, то функция sigsuspend() не возвращается. Если
сигнал генерируется, а потом обрабатывается, то sigsuspend() выдает –1 после воз­
вращения обработчика сигнала, присваивая errno значение EINTR. Если set являет­
ся недопустимым указателем, то errno присваивается значение EFAULT.
Обычный сценарий применения функции sigsuspend() связан с получением
сигналов, которые могли прибыть и оказаться заблокированными, пока программа
проходила критический регион. Процесс сначала использует sigprocmask(), чтобы
заблокировать набор сигналов, и сохраняет старую маску в oldset. Когда критиче­
ский регион останется позади, процесс вызовет sigsuspend() и предоставит для set
значение oldset.

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

386

Глава 10. Сигналы

вашего обработчика, а также для получения разнообразной информации о системе
и о состоянии процесса в момент порождения сигнала:
#include
int sigaction (int signo,
const struct sigaction *act,
struct sigaction *oldact);

Вызов функции sigaction() изменяет поведение сигнала, обозначенного аргу­
ментом signo. Этот аргумент может принимать любые значения, кроме тех, что
ассоциируются с SIGKILL и SIGSTOP. Если act не равно NULL, то этот системный вызов
изменяет актуальное поведение сигнала так, как это указано в act. Если oldact не
равно NULL, то вызов сохраняет там предшествующее (а в случае NULL — актуальное)
поведение указанного сигнала.
Структура sigaction обеспечивает детализированный контроль над сигналами.
Заголовок , включаемый из , определяет эту структуру
следующим образом:
struct sigaction {
void (*sa_handler)(int);
/* обработчик сигнала или действие */
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
/* сигналы, которые следует блокировать */
int sa_flags;
/* флаги */
void (*sa_restorer)(void); /* устаревшее поле, не соответствует POSIX */
};

В поле sa_handler указывается действие, которое должно быть осуществлено
после получения сигнала. Как и в случае с функцией signal(), это поле может быть
снабжено флагом SIG_DFL, обозначающим действие, заданное по умолчанию, флагом
SIG_IGN, при котором ядро игнорирует сигнал для процесса, либо указателем на
функцию, обрабатывающую сигнал. Эта функция имеет такой же прототип, как
и обработчик сигналов, устанавливаемый функцией signal():
void my_handler (int signo);

Если флаг SA_SIGINFO установлен в поле sa_flags, то именно поле sa_sigaction,
а не sa_handler определяет ту функцию, которая будет обрабатывать сигнал. Про­
тотип этой функции немного отличается от предыдущего:
void my_handler (int signo, siginfo_t *si, void *ucontext);

Эта функция получает номер сигнала в качестве своего первого параметра,
структуру siginfo_t — в качестве второго и структуру ucontext_t (приведенную
к указателю void) — в качестве третьего параметра. Она не имеет возвращаемого
значения. Структура siginfo_t предоставляет обработчику сигналов исчерпыва­
ющую информацию, с которой мы познакомимся чуть ниже.
Обратите внимание: в некоторых машинных архитектурах (и, возможно, в дру­
гих системах UNIX) поля sa_handler и sa_sigaction находятся в объединении, и вы
не должны присваивать значения обоим полям.

Расширенное управление сигналами

387

В поле sa_mask предоставляется набор сигналов, которые система должна блокиро­
вать на протяжении выполнения обработчика сигнала. Таким образом, программист
может самостоятельно налагать нужную защиту от повторного входа сразу для многих
обработчиков сигналов. Сигнал, обрабатываемый в текущий момент, также блокиру­
ется, если в поле sa_flags не установлен флаг SA_NODEFER. Вы не можете блокировать
SIGKILL или SIGSTOP. Вызов будет бесшумно игнорировать любой из них в sa_mask.
Поле sa_flags — это битовая маска, включающая нуль, один или несколько
флагов, изменяющих обработку сигнала, указанного как signo. Мы уже познако­
мились с флагами SA_SIGINFO и SA_NODEFER. В поле sa_flags также могут находиться
следующие значения.
 SA_NOCLDSTOP — если signo соответствует сигнал SIGCHLD, этот флаг приказывает

системе не выдавать уведомления, когда дочерний процесс останавливает или
возобновляет работу.
 SA_NOCLDWAIT — если signo соответствует сигнал SIGCHLD, этот флаг обеспечивает

автоматическое снятие дочерних процессов: при завершении дочерние процес­
сы не превращаются в зомби, а родителю не приходится вызывать для них wait()
(более того, он и не может этого сделать). В гл. 5 подробно рассказано о дочер­
них процессах, процессах-зомби и вызове wait().
 SA_NOMASK — этот флаг является устаревшим эквивалентом SA_NODEFER, не поддер­
живаемым в POSIX (флаг SA_NODEFER рассмотрен выше в этой главе). Вместо
SA_NOMASK используйте SA_NODEFER, но не удивляйтесь, если встретите такое зна­

чение в старом коде.
 SA_ONESHOT — этот флаг является устаревшим эквивалентом SA_RESETHAND , не
поддерживаемым в POSIX (флаг SA_RESETHAND был рассмотрен выше в этой
главе). Вместо SA_ONESHOT используйте SA_RESETHAND, но не удивляйтесь, если

встретите такое значение в старом коде.
 SA_ONSTACK — этот флаг приказывает системе вызвать указанный обработчик сигналов
для альтернативного сигнального стека, предоставляемого функцией sigaltstack().

Если вы не предоставите альтернативный стек, то будет использоваться стек, задан­
ный по умолчанию. Соответственно, система будет действовать так, словно этот флаг
вообще отсутствует. Альтернативные сигнальные стеки встречаются редко, хотя они
и полезны в некоторых Pthreads-приложениях с небольшими стеками потоков,
которые могут переполняться в результате определенных действий обработчика
сигналов. Мы не будем более подробно обсуждать sigaltstack() в этой книге.
 SA_RESTART — этот флаг обеспечивает перезапуск в стиле BSD для тех системных

вызовов, выполнение которых было прервано сигналами.
 SA_RESETHAND — сигнал активизирует «одноразовый» режим. Как только возвра­

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

388

Глава 10. Сигналы

В случае успеха вызов sigaction() возвращает 0. При ошибке вызов возвра­
щает –1 и присваивает errno одно из следующих значений:
 EFAULT — act или oldact содержит недопустимый указатель;
 EINVAL — signo содержит недопустимый сигнал, SIGKILL или SIGSTOP.

Структура siginfo_t
Структура siginfo_t также определяется в следующим образом:
typedef struct siginfo_t {
int si_signo;
/* номер сигнала */
int si_errno;
/* значение errno */
int si_code;
/* код сигнала */
pid_t si_pid;
/* идентификатор pid отсылающего процесса */
uid_t si_uid;
/* реальный идентификатор uid отсылающего процесса */
int si_status;
/* значение выхода или сигнал */
clock_t si_utime; /* затраченное пользовательское время */
clock_t si_stime; /* затраченное системное время */
sigval_t si_value; /* значение полезной нагрузки сигнала */
int si_int;
/* сигнал POSIX.1b */
void *si_ptr;
/* сигнал POSIX.1b */
void *si_addr;
/* местоположение в памяти, спровоцировавшее ошибку */
int si_band;
/* событие полосы */
int si_fd;
/* дескриптор файла */
};

Эта структура изобилует информацией, передаваемой обработчику сигналов
(если вы используете sa_sigaction вместо sa_sighandler). В современных вычисли­
тельных системах сигнальная модель UNIX воспринимается многими как очень
неудобный способ выполнения межпроцессной коммуникации. Вероятно, такие
скептики просто привыкли применять signal() в тех случаях, когда следовало бы
прибегнуть к sigaction() с флагом SA_SIGINFO. Структура siginfo_t позволяет выжать
из сигналов гораздо больше функциональных возможностей.
В этой структуре немало интересных данных. В частности, здесь мы находим
информацию о процессе, который отправил сигнал, и о причине этого сигнала.
Ниже приводится подробное описание каждого из полей этой структуры.
 si_signo — номер интересующего нас сигнала. Первый аргумент вашего обра­

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

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

поля. Поле действительно для всех сигналов.
 si_pid — для SIGCHLD предоставляет pid-идентификатор того процесса, который

был завершен.

Расширенное управление сигналами

389

 si_status — для SIGCHLD предоставляет статус выхода того процесса, который был

завершен.
 si_utime — для SIGCHLD предоставляет затраченное пользовательское время того

процесса, который был завершен.
 si_stime — для SIGCHLD предоставляет затраченное системное время того процес­

са, который был завершен.
 si_value — объединение si_int и si_ptr.
 si_int — для сигналов, посланных посредством функции sigqueue(), предостав­

ленная полезная нагрузка типизируется как целое число.
 si_ptr — для сигналов, посланных посредством функции sigqueue(), предостав­
ленная полезная нагрузка типизируется как указатель void.
 si_addr — для сигналов SIGBUS, SIGFPE, SIGILL, SIGSEGV и SIGTRAP этот указатель void
содержит номер ошибки, вызвавшей проблему. Например, в случае SIGSEGV в этом

поле содержится тот адрес, в котором произошло нарушение доступа к памяти
(следовательно, здесь нередко встречается NULL!).
 si_band — для сигнала SIGPOLL здесь содержится внеполосная и приоритетная
информация для того файлового дескриптора, который указан в si_fd.
 si_fd — для сигнала SIGPOLL здесь содержится дескриптор того файла, чья опе­

рация завершилась.
Поля si_value, si_int и si_ptr особенно сложны в работе, так как процесс может
использовать их для передачи произвольных данныхдругому процессу. Следова­
тельно, вы можете задействовать их, чтобы передавать как простое целое число, так
и указатель на структуру данных (кстати, в данном случае указатель вам практи­
чески не поможет, если процессы не разделяют общего адресного пространства).
Эти поля подробно рассматриваются в разд. «Отправка сигнала с полезной нагруз­
кой» далее.
POSIX гарантирует, что лишь три поля будут действительны для всех сигналов.
Доступ к остальным полям должен происходить лишь при обработке применимо­
го с ними сигнала. Например, вы должны обращаться к полю si_fd только при
обработке сигнала SIGPOLL.

Удивительный мир si_code
Поле si_code описывает причину сигнала. Для сигналов, отправляемых пользова­
телем, это поле указывает, как был послан сигнал. Если отправителем сигнала
является ядро, то поле объясняет, почему был послан сигнал.
Следующие значения поля si_code действительны для любого сигнала. Они
указывают, как/почему был послан сигнал:
 SI_ASYNCIO — отправлен по причине завершения асинхронного ввода/вывода

(см. гл. 5);
 SI_KERNEL — сгенерирован ядром;

390

Глава 10. Сигналы

 SI_MESGQ — отправлен в результате изменения состояния очереди сообщений

POSIX (в этой книге не рассматривается);
 SI_QUEUE — отправлен функцией sigqueue() (см. в следующем разделе);
 SI_TIMER — отправлен, поскольку истекло время на таймере POSIX;
 SI_TKILL — послан tkill() или tgkill(). Эти системные вызовы используются

библиотеками многопоточной обработки и не рассматриваются в этой книге;
 SI_SIGIO — отправлен, поскольку в очередь был поставлен SIGIO;
 SI_USER — отправлен функцией kill() или raise().

Следующие значения si_code действительны только для SIGBUS. Они указывают
тип произошедшей аппаратной ошибки:
 BUS_ADRALN — процесс вызвал ошибку выравнивания (выравнивание рассматри­

вается в гл. 9);
 BUS_ADRERR — процесс обратился по недействительному физическому адресу;
 BUS_OBJERR — процесс вызвал какую-то другую аппаратную ошибку.

При работе с SIGCHLD следующие значения указывают, что сделал потомок для
генерации сигнала, отосланного его предку:
 CLD_CONTINUED — работа потомка была остановлена, но позже возобновилась;
 CLD_DUMPED — произошло аварийное завершение процесса-потомка;
 CLD_EXITED — произошло обычное завершение потомка с помощью вызова
exit();
 CLD_KILLED — потомок был принудительно завершен (убит);
 CLD_STOPPED — потомок был остановлен;
 CLD_TRAPPED — потомок попал в ловушку.

Следующие значения допустимы только для SIGFPE. Они описывают тип про­
изошедшей арифметической ошибки:
 FPE_FLTDIV — процесс выполнил действие над числом с плавающей точкой, в ре­

зультате чего произошло деление на нуль;
 FPE_FLTOVF — процесс выполнил действие над числом с плавающей точкой, в ре­

зультате чего произошло переполнение;
 FPE_FLTINV — процесс выполнил недопустимое действие над числом с плавающей

точкой;
 FPE_FLTRES — процесс выполнил операцию над числом с плавающей точкой,

в результате чего был получен неточный или недопустимый результат;
 FPE_FLTSUB — процесс выполнил операцию над числом с плавающей точкой, что

привело к появлению нижнего индекса за пределами допустимого диапазона;
 FPE_FLTUND — процесс выполнил операцию с плавающей точкой, которая приве­

ла к выходу за нижнюю границу;
 FPE_INTDIV — процесс выполнил целочисленную операцию, которая привела к де­

лению на нуль;

Отправка сигнала с полезной нагрузкой

391

 FPE_INTOVF — процесс выполнил действие над целым числом, в результате чего

произошло переполнение.
Следующие значения si_code допустимы только для SIGILL. Они описывают суть
той недопустимой инструкции, которая была выполнена:
 ILL_ILLADR — процесс попытался перейти в недопустимый режим адресации;
 ILL_ILLOPC — процесс попытался выполнить недопустимый код операции;
 ILL_ILLOPN — процесс попытался выполнить недопустимый операнд;
 ILL_PRVOPC — процесс попытался выполнить привилегированный код опера­

ции;
 ILL_PRVREG — процесс попытался сработать в привилегированном регистре;
 ILL_ILLTRP — процесс попытался выполнить недопустимое прерывание.

Во всех этих значениях si_addr указывает на адрес, по которому произошло
нарушение.
При работе с SIGPOLL следующие значения указывают событие ввода/вывода,
в результате которого был сгенерирован сигнал:
 POLL_ERR — возникла ошибка ввода/вывода;
 POLL_HUP — устройство зависло либо произошло разъединение с сокетом;
 POLL_IN — в файле есть данные, доступные для считывания;
 POLL_MSG — доступно сообщение;
 POLL_OUT — в данный файл может быть записана информация;
 POLL_PRI — в файле доступны для считывания высокоприоритетные данные.

Следующие значения действительны для SIGSEGV. Они описывают два типа
недопустимых обращений к памяти:
 SEGV_ACCERR — процесс обратился к доступной области памяти недопустимым

образом, то есть нарушил права доступа к памяти;
 SEGV_MAPERR — процесс обратился к недопустимой области памяти.

При любом из этих двух значений в si_addr содержится адрес, по которому
произошло нарушение.
При работе с сигналом SIGTRAP применяются следующие два значения si_code,
описывающие тип возникшей ловушки:
 TRAP_BRKPT — процесс дошел до точки останова;
 TRAP_TRACE — процесс столкнулся с прерыванием трассировки.

Обратите внимание: si_code — это поле со значением, а не битовое поле.

Отправка сигнала с полезной нагрузкой
Как было показано в предыдущем разделе, обработчикам сигнала, зарегистриро­
ванным с флагом SA_SIGINFO, передается параметр siginfo_t. В этой структуре

392

Глава 10. Сигналы

содержится поле si_value, которое может содержать опциональную полезную
нагрузку, передаваемую от генератора сигнала к получателю сигнала.
Функция sigqueue(), стандартизированная в POSIX, позволяет процессу от­
правлять сигнал с такой полезной нагрузкой:
#include
int sigqueue (pid_t pid,
int signo,
const union sigval value);

Функция sigqueue() работает примерно так же, как и kill(). В случае успеха
сигнал, идентифицированный как signo, ставится в очередь процесса или группы
процессов, имеющих идентификатор pid, а функция возвращает 0. Полезная на­
грузка сигнала задается в value, которое представляет собой объединение (union)
целого числа и указателя void:
union sigval {
int sival_int;
void *sival_ptr;
};

При ошибке вызов возвращает –1 и присваивает errno одно из следующих зна­
чений:
 EAGAIN — вызывающий процесс достиг предельного количества сигналов, которые

могут быть поставлены в очередь;
 EINVAL — сигнал, указанный signo, недействителен;
 EPERM — вызывающий процесс не имеет достаточных прав, чтобы передать сигнал

любому из запрошенных процессов. Права доступа для отправки сигнала такие
же, как и при работе с kill() (см. разд. «Отправка сигнала» этой главы);
 ESRCH — процесс или группа процессов, обозначенные pid, не существуют, либо

в случае с процессом мы имеем дело с зомби.
Как и при работе с kill(), вы можете передать signo нулевой сигнал, чтобы про­
тестировать права доступа.
Пример сигнала с полезной нагрузкой. В этом примере мы посылаем процессу
с pid 1722 сигнал SIGUSR2. В качестве полезной нагрузки сигнал несет целочислен­
ное значение 404:
sigval value;
int ret;
value.sival_int = 404;
ret = sigqueue (1722, SIGUSR2, value);
if (ret)
perror ("sigqueue");

Изъян в UNIX?

393

Если процесс 1722 обслужит SIGUSR2 с помощью обработчика SA_SIGINFO, то об­
наружит signo со значением SIGUSR2, si->si_int со значением 404 и si->si_code со
значением SI_QUEUE.

Изъян в UNIX?
У сигналов дурная репутация среди многих UNIX-программистов. Это не просто
старый, а устаревший механизм обмена информацией между пользователем и ядром,
и в лучшем случае сигналы могут считаться примитивной формой межпроцессной
коммуникации. В мире многопоточных программ и циклов событий сигналы ка­
жутся анахронизмом. UNIX — операционная система, которая выдержала столь
серьезную проверку временем и до сих пор развивает ту самую парадигму про­
граммирования, что действовала в ней с первых дней существования. В ней сиг­
налы воспринимаются как досадное недоразумение. Я бы не замахнулся на то,
чтобы «немного переделать сигналы», но начал бы с создания нового решения на
базе файловых дескрипторов, которое было бы более выразительным, расширя­
емым, потокобезопасным.
Тем не менее хорошо это или нет, но мы по-прежнему довольствуемся сигнала­
ми. Сигналы — единственный механизм для получения многих уведомлений от
ядра (например, уведомлений о выполнении недопустимого кода операции). Кро­
ме того, именно с помощью сигналов в UNIX (а значит, и в Linux) завершаются
процессы и организуются взаимоотношения «предок — потомок». Следовательно,
программист должен понимать сигналы и уметь ими пользоваться.
Одна из основных причин, по которым качество работы с сигналами все сильнее
падает, — сложность написания подходящего обработчика сигналов, с которым не
возникали бы проблемы из области реентерабельности. Но если вы будете держать
обработчики максимально простыми и будете пользоваться лишь теми функциями,
что перечислены в табл. 10.3 (если вообще решите применять функции), то все
должно быть безопасным.
Еще одна брешь в броне сигналов состоит в том, что многие программисты попрежнему предпочитают использовать для управления ими вызовы signal() и kill(),
а не sigaction() и sigqueue(). Как вы могли убедиться в двух последних разделах,
сигналы получаются гораздо более мощными и выразительными, если применять
обработчики в стиле SA_SIGINFO. Хотя я не являюсь поклонником сигналов, следу­
ет признать, что большинство их острых углов удается обходить, пользуясь про­
двинутыми интерфейсами Linux. Правда, капризность сигналов при этом никуда
не девается.

11

Время

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

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

Фактическое время. Это точное время и дата из нашего реального мира

-

то

самое время, которое вы можете проверить по настенным часам. Процессы

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

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

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

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

Linux,

за единицу фактического времени

обычно протекает менее единицы процессного времени. Но бывает и наоборот:
при наличии нескольких процессоров, а также какого-то числа потоков в про­

цессе процессное время для отдельно взятой операции может превышать фак­
тическое время!

О Монотонное время. Время такого рода течет по строго линейному принципу.

В большинстве операционных систем, в том числе в

Linux,

в таком качестве

используется время доступности системы (время, истекшее с момента загрузки

системы). Фактическое время в компьютере может изменяться

-

например,

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

-

например, иногда

теряются секунды. С другой стороны, период доступности системы есть детер­

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

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

между двумя дискретными выборками.

Глава 11. Время

395

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

определенной контрольной точки, например от настоящего момента: «через
5 секунд» или «10 минут назад». Для подсчета относительного времени исполь­
зуется монотонное время.
 Абсолютное время. В данном случае мы говорим о времени, которое опре­

деляется без такой контрольной точки; например, полдень 25 марта 1968 года.
Для отсчета таких временных значений идеально подходит фактическое
время.
На практике используется как относительное, так и абсолютное время. Напри­
мер, процессу может потребоваться отменить запрос через 500 миллисекунд, об­
новлять экран 60 раз в секунду или отметить, что с момента начала операции уже
истекло 7 секунд. Во всех этих случаях применяется относительное время. Напро­
тив, приложение-календарь может сохранить дату пользовательской вечеринки
как 8 февраля, а файловая система полностью запишет время и дату создания
файла (а не «5 секунд назад»). При этом на часах пользователя будет отображать­
ся дата по григорианскому календарю, а не количество секунд, истекших с момен­
та загрузки системы.
В системах UNIX абсолютное время представляется как количество секунд,
истекших с момента epoch, который наступил в 00:00:00 по Гринвичу 1 января
1970 года. Как ни странно, но это означает, что в UNIX на самом низком уровне
абсолютное время также является относительным. В UNIX существует специаль­
ный тип данных для хранения «секунд, истекших с момента epoch». Этот формат
данных мы рассмотрим в следующем разделе.
Операционные системы отсчитывают ход времени с помощью программных
часов. Это часы, ход которых программно поддерживается ядром. Ядро инстанци­
рует периодический таймер, который называется системным и тикает с определен­
ной частотой. Когда интервал таймера заканчивается, ядро увеличивает количе­
ство истекшего времени на единицу, называемую тактом или циклом. Ранее такты
были 32-битными значениями, но, начиная с версии ядра Linux 2.6, они стали
64-битными1.
В Linux рабочая частота системного таймера называется HZ, поскольку именно
такое самоочевидное имя определяет для нее препроцессор. Значение HZ зависит
от конкретной архитектуры и не является частью Linux ABI. Следовательно, про­
грамма не может зависеть от того или иного значения частоты или ориентировать­
ся на него. Исторически в архитектуре x86 использовалось значение 100, то есть
система делала 100 тактов в секунду, а системный таймер имел частоту 100 Герц.
Таким образом, каждый такт занимал 0,01 секунды. В версии 2.6 ядра Linux раз­
работчики взвинтили значение HZ сразу до 1000, продолжительность такта там
составляла всего 0,001 секунды. Но в версии 2.6.13 и выше значение HZ равно 250
1

В настоящее время ядро Linux поддерживает и «бестактовый» режим работы.

396

Глава 11. Время

(такт занимает 0,004 секунды1). Значение HZ по определению заключает в себе
компромисс: чем больше значение, тем выше его разрешающая способность и тем
выше издержки, связанные с таймером.
Хотя процессы и не должны опираться на какое-либо фиксированное значение
HZ, POSIX описывает механизм, позволяющий во время выполнения определять
частоту системного таймера:
long hz;
hz = sysconf (_SC_CLK_TCK);
if (hz == –1)
perror ("sysconf"); /* этого происходить не должно */

Этот интерфейс полезен, если программе требуется определить разрешение
системного таймера, но он не нужен для преобразования системных значений
времени в секунды. Дело в том, что большинство интерфейсов POSIX экспорти­
руют временные величины, которые уже преобразованы в определенную частоту
или масштабированы в нее. Данная частота не зависит от значения HZ. В отличие
от HZ эта фиксированная частота входит в состав системного ABI; в архитектуре
x86 ее значение равно 100. Функции POSIX, возвращающие время в пересчете на
такты таймера, используют CLOCKS_PER_SEC для представления фиксированной
частоты.
Иногда бывает и так, что компьютер неожиданно отключается. Иногда даже
вилку вынимают из розетки, но после загрузки компьютерное время оказывается
верным. Дело в том, что на большинстве компьютеров установлены работающие
от батареи аппаратные часы, на которых отслеживается дата и время, пока ком­
пьютер остается выключен. Когда ядро загружается, оно инициализирует концепцию
текущего времени, исходя из показаний аппаратных часов. Аналогично, когда
пользователь выключает систему, ядро записывает текущее время на аппаратные
часы. Системный администратор может синхронизировать время и в другие мо­
менты с помощью команды hwclock.
Управление ходом времени в системе UNIX связано с решением нескольких
задач, причем для любого отдельно взятого процесса актуальны лишь некоторые
из них. В частности, к таким задачам относятся установка и определение текущего
фактического времени, «засыпание» на определенный период времени, выполнение
высокоточных измерений времени и управление таймерами. В этой главе мы об­
судим все разнообразные рутинные процедуры, связанные с обслуживанием хода
времени. Для начала рассмотрим структуры данных, применяемые в Linux для
представления времени.
1

В настоящее время HZ является одним из параметров компиляции ядра, причем в архи­
тектуре x86 можно выбрать любое из следующих значений: 100, 250, 300 и 1000. Тем не
менее программы из пользовательского пространства не могут зависеть от конкретного
значения HZ.

Структуры данных, связанные с представлением времени

397

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

Оригинальное представление
Простейшая структура данных называется time_t, она определяется в заголовке
. Предполагалось, что time_t будет непрозрачным типом. Однако в боль­
шинстве систем UNIX, в том числе в Linux, он является простым определением
типа long из языка C:
typedef long time_t;
time_t представляет количество секунд, истекших с момента epoch. «Это ведь
так много, тут наверняка возникнет переполнение!» — подумают многие чита­
тели. На самом деле до переполнения еще далеко, но оно действительно может
возникнуть, так как UNIX-системы будут еще долго использоваться. При рабо­
те с 32-битным числовым типом long структура time_t может представлять до
2 147 483 647 секунд, истекших с момента epoch. Таким образом, новая «пробле­
ма 2000 года» ожидает нас в обозримом будущем — это будет 2038 год. Остается
надеяться, что в 22:14:07 в понедельник 18 января 2038 года большинство систем
и программ уже будут 64-битными.

А теперь — с микросекундной точностью!
Серьезная проблема, связанная с time_t, заключается в том, что за одну секунду
может произойти довольно много событий. Структура timeval расширяет time_t,
обеспечивая точность до микросекунд. В заголовке эта структура опре­
деляется следующим образом:
#include
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};

/* секунды */
/* микросекунды */

Поле tv_sec измеряет секунды, а tv_usec — микросекунды. Странное поле
suseconds_t обычно является определением целочисленного типа.

398

Глава 11. Время

И еще лучше: наносекундная точность
Иногда бывает недостаточно и микросекундного разрешения, и структура timespec
поднимает планку до наносекундной точности. В заголовке эта структура
определяется следующим образом:
#include
struct timespec {
time_t tv_sec;
long tv_nsec;
};

/* секунды */
/* наносекунды */

При наличии выбора наносекундное разрешение обычно предпочтительнее микро­
секундного. Кроме того, структура timespec отказалась от глупого компонента
suseconds_t в пользу простого и нетребовательного long. Таким образом, после появ­
ления структуры timespec на нее переключилось большинство интерфейсов, приме­
няемых при работе с временем, и общая точность измерений выросла. Тем не менее,
как мы вскоре увидим, одна важная функция по-прежнему использует timeval.
На практике ни одна из структур обычно не обеспечивает заявленной точности,
так как наносекундное и даже микросекундное разрешение недостижимо для си­
стемного таймера. Тем не менее предпочтительно иметь в интерфейсе информацию
о разрешении, чтобы он мог приспособиться к тому разрешению, которое действи­
тельно обеспечивается в системе.

Разбиение времени
Функции, которые мы рассмотрим, выполняют преобразования между временем
UNIX и строками либо программно строят строковое представление указанной
даты. Для облегчения этого процесса стандарт C описывает структуру tm. Она по­
зволяет выражать «разбитое» время в формате, более удобочитаемом для человека.
Эта структура также определяется в заголовке :
#include
struct tm {
int tm_sec;
/* секунды */
int tm_min;
/* минуты */
int tm_hour;
/* часы */
int tm_mday;
/* день месяца */
int tm_mon;
/* месяц */
int tm_year;
/* год */
int tm_wday;
/* день недели */
int tm_yday;
/* день года */
int tm_isdst; /* летнее время? */
#ifdef _BSD_SOURCE
long tm_gmtoff; /* временной пояс, коррекция относительно Гринвича */
const char *tm_zone; /* сокращенное обозначение временного пояса */
#endif /* _BSD_SOURCE */
};

Структуры данных, связанные с представлением времени

399

Структура tm помогает с большей определенностью судить, к какому именно дню
относится значение структуры time_t, равное, например, 314159: к воскресенью или
к субботе (правильный ответ — суббота). Учитывая, как много места она занимает,
это, конечно, не лучший вариант представления даты и времени. Но она удобна для
преобразования значений из пользовательского формата в машинный и наоборот.
Вот краткое описание полей этой структуры:
 tm_sec — количество секунд, истекших с момента окончания последней минуты.

Обычно это значение находится в диапазоне от 0 до 59, но может достигать и 61,
чтобы учитывать до двух корректировочных секунд;
 tm_min — количество минут, истекших с момента окончания последнего часа.

Это значение находится в диапазоне от 0 до 59;
 tm_hour — количество часов, истекших после полуночи. Это значение находится

в диапазоне от 0 до 23;
 tm_mday — день месяца. Это значение находится в диапазоне от 0 до 31. Стандарт
POSIX не указывает значение 0, однако в Linux оно используется для послед­

него дня предыдущего месяца;
 tm_mon — количество месяцев, истекших с января. Это значение находится в диа­

пазоне от 0 до 11;
 tm_year — количество лет, истекших с 1900-го;
 tm_wday — количество дней, истекших с воскресенья. Это значение находится

в диапазоне от 0 до 6;
 tm_yday — количество дней, истекших с 1 января. Это значение находится в диа­

пазоне от 0 до 365;
 tm_isdst — особое поле, указывающее, должен ли учитываться переход на летнее

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

гринвичского времени. Это поле присутствует в структуре лишь при условии,
что значение _BSD_SOURCE будет определено до включения ;
 tm_zone — сокращенное обозначение актуального временного пояса — например,
EST. Это поле присутствует в структуре лишь при условии, что значение _BSD_SOURCE
будет определено до включения .

Тип для процессного времени
Тип clock_t представляет такты процессорных часов. Это целочисленный тип, за­
частую long. Такты, представляемые clock_t, в зависимости от применяемого ин­
терфейса, могут соответствовать либо фактической частоте системного таймера
(HZ), либо CLOCKS_PER_SEC.

400

Глава 11. Время

Часы POSIX
Некоторые системные вызовы, рассматриваемые в этой главе, используют часы
POSIX — стандарт, применяемый для реализации и представления источников
времени. Тип clockid_t соответствует конкретной разновидности часов POSIX,
в Linux поддерживается пять таких разновидностей.
 CLOCK_REALTIME — часы реального (фактического) времени, действующие в пре­

делах всей системы. Установка этих часов требует специальных привиле­
гий.
 CLOCK_MONOTONIC — часы с монотонным отсчетом времени, которые не может

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

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

доставляемые процессором. Например, в архитектуре x86 они используют ре­
гистр счетчика цифровых меток (TSC).
 CLOCK_THREAD_CPUTIME_ID — напоминают попроцессные часы, но уникальны для

каждого потока в процессе.
Стандарт POSIX требует наличия только одних часов — CLOCK_REALTIME. Поэто­
му, хотя Linux и обеспечивает нормальную работу всех пяти часов, в коде, предна­
значенном для портирования, следует использовать только CLOCK_REALTIME.
Разрешение источника времени. Стандарт POSIX определяет функцию
clock_getres(), позволяющую установить разрешение конкретного источника
времени:
#include
int clock_getres (clockid_t clock_id,
struct timespec *res);

Успешный вызов clock_getres() сохраняет в res разрешение часов, указанное
в clock_id, и, если оно не равно NULL, вызов возвращает 0. При ошибке функция
возвращает –1 и устанавливает errno в один из следующих двух кодов ошибок:
 EFAULT — res является недопустимым указателем;
 EINVAL — clock_id не является допустимым источником времени, применяемым

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

Получение текущего времени суток

401

clockid_t clocks[] = {
CLOCK_REALTIME,
CLOCK_MONOTONIC,
CLOCK_PROCESS_CPUTIME_ID,
CLOCK_THREAD_CPUTIME_ID,
CLOCK_MONOTONIC_RAW,
(clockid_t) –1 };
int i;
for (i = 0; clocks[i] != (clockid_t) –1; i++) {
struct timespec res;
int ret;
ret = clock_getres (clocks[i], &res);
if (ret)
perror ("clock_getres");
else
printf ("время=%d секунд=%ld наносекунд=%ld\n",
clocks[i], res.tv_sec, res.tv_nsec);
}

В современной системе с архитектурой x86 получим примерно такой вывод:
clock=0
clock=1
clock=2
clock=3
clock=4

sec=0
sec=0
sec=0
sec=0
sec=0

nsec=4000250
nsec=4000250
nsec=1
nsec=1
nsec=4000250

Обратите внимание: 4 000 250 наносекунд здесь равны 4 миллисекундам, или
0,004 секунды. В свою очередь, разрешение 0,004 секунды обусловлено работой си­
стемных часов в архитектуре x86, значение HZ для которых равно 250 (об этом мы
говорили в начале главы). Следовательно, мы видим, что оба значения — CLOCK_REALTIME
и CLOCK_MONOTONIC — привязаны к тактам процессора и к разрешению, обеспечиваемо­
му системным таймером. Напротив, и CLOCK_PROCESS_CPUTIME_ID, и CLOCK_PROCESS_
CPUTIME_ID используют источник времени с более высоким разрешением. На данной
машине с архитектурой x86 таким источником является TSC, который, как было
показано выше, обеспечивает разрешение с наносекундной точностью.
В Linux, а также в большинстве других UNIX-систем все функции, использу­
ющие часы POSIX, требуют связывания с результирующим объектом посредством
librt. Например, если скомпилировать предыдущий фрагмент кода в исполняемый
вид, то у нас может получиться следующая команда:
$ gcc -Wall -W -O2 -lrt -g -o snippet snippet.c

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

402

Глава 11. Время

информацию пользователю, подсчитывать истекшее или относительное время,
снабжать событие цифровой печатью и т. д. Простейшим и исторически наиболее
распространенным инструментом для получения актуального времени является
функция time():
#include
time_t time (time_t *t);

Вызов time() возвращает текущее время, выраженное в количестве секунд, ис­
текших с момента epoch. Если параметр t не равен NULL, то функция также записы­
вает актуальное время в предоставленный указатель.
При ошибке функция возвращает –1 (с приведением типа к time_t) и присваи­
вает errno соответствующее значение. Единственной возможной ошибкой в данном
случае является EFAULT. Вы получите такое значение, если указатель t является
недопустимым.
Например:
time_t t;
printf ("текущее значение: %ld\n", (long) time (&t));
printf ("то же значение: %ld\n", (long) t);
ПРИМЕЧАНИЕ
Представление времени в поле time_t, выражаемое как «количество секунд, истекших с момента epoch», не соответствует точному количеству секунд, прошедших с того судьбоносного момента времени. В действующей в UNIX системе подсчета времени високосными считаются все
годы, делящиеся на четыре, а корректировочные секунды просто игнорируются. Поэтому значение времени в time_t не совсем точное. Но оно хотя бы является согласованным — и этого
достаточно.

Более удобный интерфейс
Функция gettimeofday() расширяет time(), обеспечивая разрешение с точностью до
миллисекунд:
#include
int gettimeofday (struct timeval *tv,
struct timezone *tz);

При успешном вызове gettimeofday() текущее время записывается в структуру
timeval, на которую направлен указатель tv, после чего функция возвращает 0.
Структура timezone и параметр tz уже выходят из употребления; в Linux их не
следует применять. Для tz всегда передавайте значение NULL.
При ошибке этот вызов возвращает –1 и присваивает errno значение EFAULT. Это
единственная возможная ошибка; она говорит о том, что указатель tv или tz недо­
пустим.

Получение текущего времени суток

403

Например:
struct timeval tv;
int ret;
ret = gettimeofday (&tv, NULL);
if (ret)
perror ("gettimeofday");
else
printf ("секунды=%ld микросекунды=%ld\n",
(long) tv.sec, (long) tv.tv_usec);

Структура timezone выходит из употребления, поскольку ядро при работе не учи­
тывает временные пояса, а glibc отказывается работать с полем tz_dsttime структуры
timezone. Работа с временными поясами будет рассмотрена в следующем разделе.

Продвинутый интерфейс
Стандарт POSIX предоставляет интерфейс clock_gettime() для получения значения
времени из конкретного источника. Но эта функция гораздо интереснее по той
причине, что теоретически работает с точностью до наносекунд:
#include
int clock_gettime (clockid_t clock_id,
struct timespec *ts);

В случае успеха вызов возвращает 0 и сохраняет текущее значение источни­
ка времени, указанное с помощью clock_id в ts. При ошибке этот вызов возвра­
щает –1 и присваивает errno одно из следующих значений:
 EFAULT — ts является недопустимым указателем;
 EINVAL — в данной системе clock_id является недопустимым источником вре­

мени.
В следующем примере мы получаем актуальное время из всех четырех стандарт­
ных источников времени:
clockid_t clocks[] = {
CLOCK_REALTIME,
CLOCK_MONOTONIC,
CLOCK_PROCESS_CPUTIME_ID,
CLOCK_THREAD_CPUTIME_ID,
CLOCK_MONOTONIC_RAW,
(clockid_t) –1 };
int i;
for (i = 0; clocks[i] != (clockid_t) –1; i++) {
struct timespec ts;
int ret;
ret = clock_gettime (clocks[i], &ts);

404

Глава 11. Время

if (ret)
perror ("clock_gettime");
else
printf ("часы=%d секунды=%ld наносекунды=%ld\n",
clocks[i], ts.tv_sec, ts.tv_nsec);
}

Получение процессного времени
Системный вызов times() получает процессное время работающего процесса и его
потомков. Значение времени выражается в тактах процессора:
#include
struct tms {
clock_t tms_utime;
clock_t tms_stime;
clock_t tms_cutime;
clock_t tms_cstime;
};

/*
/*
/*
/*

потребленное пользовательское время */
потребленное системное время */
пользовательское время, потребленное потомками */
системное время, потребленное потомками */

clock_t times (struct tms *buf);

В случае успеха вызов заполняет предоставленную структуру tms, на которую
направлен указатель buf, записывая в нее значения процессного времени, потреб­
ленного вызывающим процессом и его потомками. Полученное количество вре­
мени подразделяется на пользовательское и системное время. Пользовательское
время тратится на выполнение кода в пользовательском пространстве. Системное
время тратится на выполнение кода в пространстве ядра — например, в ходе си­
стемного вызова или при отказе страницы. Сообщаемые значения времени для
каждого потомка учитываются только после того, как потомок завершится, а пре­
док вызовет в процессе waitpid() или другую подобную функцию. Вызов возвра­
щает количество тактов процессора, которое монотонно повышается. Это время
истекает с произвольного, но фиксированного момента в прошлом. Ранее в каче­
стве такой точки отсчета принимался момент загрузки системы — следовательно,
функция times() возвращала количество тактов, равное последнему непрерывно­
му периоду работы системы, — но в настоящее время точка отсчета отнесена при­
мерно на 429 миллионов секунд в прошлое от момента загрузки. Разработчики
реализовали это изменение, чтобы перехватывать такой системный код, который
не мог обрабатывать оборачивание системного времени и попадание в нуль. Таким
образом, абсолютное значение, возвращаемое этой функцией, практически не
имеет смысла; но относительные изменения между двумя вызовами по-прежнему
остаются актуальными.
При ошибке этот вызов возвращает –1 и присваивает errno соответствующее
значение. В Linux возможен только один код ошибки — EFAULT; он означает, что
указатель buf является недопустимым.

Установка текущего времени суток

405

Установка текущего времени суток
В предыдущих разделах мы обсуждали, как получать значения времени. Но иногда
в приложении приходится устанавливать для даты и времени определенное значе­
ние. Практически всегда для этого используется специальная процедура, созданная
именно для такой цели, например date.
Аналог time() называется stime():
#define _SVID_SOURCE
#include
int stime (time_t *t);

При успешном вызове stime() системному времени присваивается значение, на
которое указывает t, и функция возвращает 0. Вызов требует, чтобы у пользовате­
ля, который его выполняет, была возможность CAP_SYS_TIME. Как правило, такая
возможность есть только у администратора.
При ошибке вызов возвращает –1 и присваивает errno значение EFAULT (если
указатель t недопустим) или EPERM, если у вызывающего пользователя отсутствует
возможность CAP_SYS_TIME.
Применять эту функцию очень просто:
time_t t = 1;
int ret;
/* устанавливаем время в значение «одна секунда с момента epoch» */
ret = stime (&t);
if (ret)
perror ("stime");

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

Установка времени с заданной точностью
Парная функция для gettimeofday() называется settimeofday():
#include
int settimeofday (const struct timeval *tv,
const struct timezone *tz);

Успешный вызов settimeofday() присваивает времени значение, на которое
указывает tv, после чего функция возвращает 0. Как и при работе с gettimeofday(),
рекомендуется передавать tz значение NULL. При ошибке этот вызов возвращает –1
и присваивает errno одно из следующих значений:

406

Глава 11. Время

 EFAULT — tv или tz указывает на недопустимую область памяти;
 EINVAL — поле в одной из предоставленных структур содержит ошибку;
 EPERM — у вызывающего процесса отсутствует возможность CAP_SYS_TIME.

В следующем примере мы задаем для времени значение «суббота, середина
декабря 1979 года»:
struct timeval tv = { .tv_sec = 31415926,
.tv_usec = 27182818 };
int ret;
ret = settimeofday (&tv, NULL);
if (ret)
perror ("settimeofday");

Продвинутый интерфейс для установки времени
Так же как функция clock_gettime() превосходит gettimeofday(), функция clock_
settime() избавляет нас от необходимости пользоваться settimeofday():
#include
int clock_settime (clockid_t clock_id,
const struct timespec *ts);

В случае успеха этот вызов возвращает 0, а для источника, указанного как clock_id,
устанавливается время, заданное в ts. При ошибке этот вызов возвращает –1 и при­
сваивает errno одно из следующих значений:
 EFAULT — указатель ts недопустим;
 EINVAL — clock_id является в данной системе недопустимым источником вре­

мени;
 EPERM — процесс не обладает необходимыми правами доступа, чтобы установить

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

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

Эксперименты с временем
В системах UNIX и в языке C предоставляется семейство функций для преобра­
зования разделенного времени (строковое представление времени в кодировке
ASCII). Так, функция time_t.asctime() преобразует структуру tm — разделенное
время — в ASCII-строку:

Эксперименты с временем

407

#include
char * asctime (const struct tm *tm);
char * asctime_r (const struct tm *tm, char *buf);

Она возвращает указатель на статически выделенную строку. Последующий
вызов любой функции, предназначенной для работы с временем, может перезапи­
сать эту строку, поэтому asctime() небезопасна при работе с потоками.
Соответственно, для многопоточных программ (и для программистов, которые
терпеть не могут плохо сработанные интерфейсы) рекомендуется использовать
функцию asctime_r(). Она не возвращает указатель на статически выделенную
строку, а использует строку, на которую направлен указатель buf. Эта строка долж­
на содержать не менее 26 символов.
В случае ошибки обе функции возвращают NULL.
Вызов mktime() также преобразует структуру tm, но на этот раз в time_t:
#include
time_t mktime (struct tm *tm);

При вызове mktime() мы также задаем временной пояс с помощью tzset() так,
как указано в tm. При ошибке она возвращает –1 (с приведением типа к time_t).
Вызов ctime() преобразует time_t в соответствующее ASCII-представление:
#include
char * ctime (const time_t *timep);
char * ctime_r (const time_t *timep, char *buf);

При ошибке функция возвращает NULL. Например:
time_t t = time (NULL);
printf ("время всего лишь одну строку назад: %s", ctime (&t));

Обратите внимание: здесь отсутствует переход на новую строку. Может быть,
это и неудобно, но ctime() прикрепляет новую строку к своей возвращенной
строке.
Как и asctime(), ctime() возвращает указатель на статическую строку. По­
скольку эта функция небезопасна при использовании потоков, в многопоточных
программах вместо нее требуется применять ctime_r(). Эта функция оперирует
буфером, предоставленным в buf. Длина буфера должна составлять не менее
26 символов.
Функция gmtime() преобразует полученное значение time_t в структуру tm, вы­
раженную в контексте временного пояса UTC:
#include
struct tm * gmtime (const time_t *timep);
struct tm * gmtime_r (const time_t *timep, struct tm *result);

408

Глава 11. Время

При ошибке она возвращает NULL. Эта функция статически выделяет возвращен­
ную структуру и, следовательно, небезопасна для потоков. В многопоточных про­
граммах необходимо использовать функцию gmtime_r(); она оперирует структурой,
на которую указывает result.
Вызовы localtime() и localtime_r() функционально напоминают gmtime()
и gmtime_r() соответственно, но они выражают полученный тип time_t в контексте
часового пояса пользователя:
#include
struct tm * localtime (const time_t *timep);
struct tm * localtime_r (const time_t *timep, struct tm *result);

Как и в случае с mktime(), вызов localtime() сопровождается вызовом tzset() и ини­
циализацией часового пояса. Необходимость такого поведения для localtime_r()
не указана.
Функция difftime() возвращает количество секунд, составляющих период
между двумя значениями time_t (ее возвращаемое значение приводится к типу
double):
#include
double difftime (time_t time1, time_t time0);

Во всех системах POSIX time_t является арифметическим типом, а difftime()
эквивалентна следующему коду (с оговоркой, что она не обнаруживает перепол­
нения при вычитании):
(double) (time1 - time0)

Поскольку в Linux time_t является целочисленным типом, нет необходимости
приводить его к double. Для обеспечения переносимости программы пользуйтесь
функцией difftime().

Настройка системных часов
Большие и внезапные скачки фактического времени могут ввергать в хаос такие
приложения, чье нормальное функционирование завязано на абсолютных значе­
ниях времени. Рассмотрим, к примеру, команду make, которая выстраивает про­
граммные проекты в соответствии с подробным описанием, заданным в Makefile.
При каждом вызове программы не происходит полной перестройки деревьев ис­
ходников. Если бы такая перестройка происходила, то при изменении единствен­
ного файла в больших программных проектах пересборка могла бы занимать до
нескольких часов. Вместо этого make просматривает временные метки, находящие­
ся в файле исходников (допустим, wolf.c), соответствующие сделанным измене­
ниям, и сравнивает их с объектным файлом (в данном случае — wolf.o). Если файл
исходников или любой из сопровождающих его файлов, например wolf.h, окажет­
ся новее объектного файла, то make перестроит файл исходников в обновленный

Настройка системных часов

409

объектный файл. Если файл исходников не окажется новее объектного файла, то
не будут предприниматься никакие действия.
Теперь представим себе следующую ситуацию. Пользователь обнаруживает,
что его часы отстали на пару часов, и запускает date, чтобы обновить системные
часы. Если после этого пользователь обновит и пересохранит wolf.c, возникнут
проблемы. Если пользователь отмотал текущее время назад, то файл wolf.c пока­
жется системе старше, чем wolf.o, хотя это и не так. Пересборка не состоится.
Чтобы не допускать такого беспорядка, UNIX предоставляет функцию adjtime(),
которая постепенно корректирует текущее время в направлении заданного пере­
вода часов. Это делается для обеспечения правильности работы фоновых процессов,
таких как демоны сетевого протокола времени (NTP), которые постоянно коррек­
тируют время с учетом рассинхронизации часов. Функция adjtime() призвана
минимизировать негативное воздействие этих демонов на систему. Периодическая
корректировка часов для преодоления рассинхронизации называется подстройкой:
#define _BSD_SOURCE
#include
int adjtime (const struct timeval *delta,
struct timeval *olddelta);

При успешном вызове функции adjtime() мы приказываем ядру начать медлен­
ную корректировку времени в направлении, указанном в параметре delta, до пол­
ного выполнения этой корректировки. Если в delta указано отрицательное значе­
ние времени, то ядро замедляет ход системных часов до тех пор,пока не будет
достигнута желаемая коррекция. Все изменения ядро применяет таким образом,
что значение часов обязательно изменяется монотонно, резких скачков времени
никогда не происходит. Даже при отрицательном показателе delta коррекция вре­
мени не связана с переводом «стрелок» назад. Просто системные часы будут «ти­
кать» медленнее, пока компьютерное время не совпадет с фактическим.
Если значение delta не равно NULL, то ядро приостанавливает обработку всех
ранее зарегистрированных корректировок. Однако если корректировки уже ча­
стично внесены, то эти изменения продолжают учитываться. Если значение olddelta
не равно NULL, то все ранее зарегистрированные, но еще не примененные корректи­
ровки записываются в предоставленную структуру timeval. Передав значение delta,
равное NULL, и допустимое значение olddelta, можно будет получить все текущие
корректировки.
Все изменения, вносимые с помощью adjtime(), должны быть небольшими.
Идеальный вариант использования функции — протокол NTP, который работает
с незначительными изменениями (не превышающими пары секунд). В обоих на­
правлениях Linux определяет максимальный и минимальный порог корректиров­
ки, оба этих порога достигают нескольких тысяч секунд.
При ошибке вызов adjtime() возвращает –1 и присваивает errno одно из следу­
ющих значений:

410

Глава 11. Время

 EFAULT — delta или olddelta является недопустимым указателем;
 EINVAL — изменение, указанное в delta, слишком мало или слишком велико;
 EPERM — вызывающий пользователь не обладает возможностью CAP_SYS_TIME.

Стандарт RFC 1305 определяет значительно более мощный и, соответственно,
более сложный алгоритм корректировки часов, чем поступательный подход, при­
меняемый adjtime(). Linux реализует этот алгоритм с помощью системного вызова
adjtimex():
#include
int adjtimex (struct timex *adj);

При вызове adjtimex() считываются параметры ядра, связанные с временем,
после чего они записываются в структуру timex, на которую указывает adj. Возмо­
жен вариант (зависящий от значения поля modes в этой структуре), когда этот вызов
может дополнительно задавать некоторые параметры.
Структура timex определяется в заголовке следующим обра­
зом:
struct timex {
int modes;
long offset;
long freq;

/*
/*
/*
/*
long maxerror;
/*
long esterror;
/*
int status;
/*
long constant;
/*
long precision;
/*
long tolerance;
/*
struct timeval time; /*
long tick;
/*

селектор режима */
смещение времени (микросекунд) */
смещение частоты */
(масштабированное значение ppm) */
максимальная ошибка (микросекунд) */
расчетная ошибка (микросекунд) */
состояние часов */
временная константа PLL */
точность часов (микросекунд) */
допустимая погрешность частоты часов (ppm) */
текущее время */
количество микросекунд между тактами часов */

};

Поле modes содержит побитовое «ИЛИ» из нуля или более следующих флагов:
 ADJ_OFFSET — задает смещение времени с помощью offset;
 ADJ_FREQUENCY — устанавливает используемую частоту с помощью freq;
 ADJ_MAXERROR — задает максимальную ошибку с помощью maxerror;
 ADJ_ESTERROR — определяет расчетную ошибку с помощью esterror;
 ADJ_STATUS — задает состояние часов с помощью status;
 ADJ_TIMECONST — устанавливает временную константу PLL (фазовая автопод­
стройка частоты) с помощью constant;
 ADJ_TICK — задает значение такта с помощью tick;
 ADJ_OFFSET_SINGLESHOT — с помощью offset однократно задает смещение времени
при применении простого алгоритма, например adjtime().

Засыпание и ожидание

411

Если значение поля modes равно 0, то никакие значения не устанавливаются.
Ненулевое значение поля modes может предоставить лишь такой пользователь,
который обладает характеристикой CAP_SYS_TIME. Любой другой пользователь может
задать для поля значение 0, получив, таким образом, все параметры, но не имея
возможности установить ни один из них.
При успехе вызов adjtimex() возвращает текущее состояние часов, которое мо­
жет соответствовать одному из следующих значений:
 TIME_OK — часы синхронизированы;
 TIME_INS — будет вставлена корректировочная секунда;
 TIME_DEL — будет удалена корректировочная секунда;
 TIME_OOP — идет корректировочная секунда;
 TIME_WAIT — корректировочная секунда только что сработала;
 TIME_BAD — часы не синхронизированы.

При ошибке вызов adjtimex() возвращает –1 и присваивает errno одно из сле­
дующих значений ошибки:
 EFAULT — аргумент adj содержит недопустимый указатель;
 EINVAL — одно из полей modes, offset либо tick содержит недопустимое значение;
 EPERM — значение modes является ненулевым, но вызывающий пользователь не
обладает характеристикой CAP_SYS_TIME.

Системный вызов adjtimex() является специфичным для Linux. Если приложе­
ние должно быть переносимым, то в данном случае следует использовать функцию
adjtime().
Стандарт RFC 1305 определяет довольно сложный алгоритм, поэтому подроб­
ное обсуждение функции adjtimex() выходит за рамки этой книги. Более развер­
нуто о ней можно почитать в документации по RFC.

Засыпание и ожидание
Различные функции позволяют процессу засыпать (приостанавливать работу)
на определенный период времени. Первая из таких функций, sleep(), переводит
указанный процесс в спящий режим на такое количество секунд, которое дано
в seconds:
#include
unsigned int sleep (unsigned int seconds);

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

412

Глава 11. Время

долго процесс находился в спящем режиме. Поэтому возвращаемое значение
также не проверяется.
sleep (7);

/* засыпание на семь секунд */

Если необходимо, чтобы процесс действительно «проспал» весь указанный
период, то вы можете продолжать вызывать sleep() с ее возвращаемым значением,
пока эта функция не вернет 0:
unsigned int s = 5;
/* спим пять секунд; именно пять и точка! */
while ((s = sleep (s)))
;

Засыпание с микросекундной точностью
Если система засыпает на период, состоящий из целого количества секунд, нас это
не устроит. В современных системах секунда — это практически вечность, поэтому
нередко в программах требуется большая временная детализация спящего режима.
Познакомьтесь с функцией usleep():
/* Версия из BSD */
#include
void usleep (unsigned long usec);
/* Версия из SUSv2 */
#define _XOPEN_SOURCE 500
#include
int usleep (useconds_t usec);

При успешном вызове usleep() мы приказываем процессу заснуть на usec микро­
секунд. К сожалению, стандарты BSD и Single UNIX Specification (SUS) по-разному
определяют прототип этой функции. Функция из BSD получает unsigned long и не
имеет возвращаемого значения. В варианте для SUS функция usleep() принимает тип
useconds_t и возвращает int. Linux следует спецификации SUS, если _XOPEN_SOURCE
имеет значение 500 или выше. Если значение _XOPEN_SOURCE не определено или ме­нее 500,
то Linux следует BSD.
Версия из SUS возвращает 0 при успехе и –1 при ошибке. Допустимыми значе­
ниями errno являются EINTR, если период сна был прерван сигналом, или EINVAL,
если значение usecs было слишком велико (в Linux допустимым считается весь
диапазон значений этого типа, поэтому такая ошибка никогда не возникает).
Согласно спецификации тип useconds_t является беззнаковым целым числом
и может содержать значения вплоть до 1 000 000.
Из-за различий между конфликтующими вариантами прототипов, а также в силу
того, что некоторые системы UNIX могут поддерживать либо один, либо другой ва­
риант, разумно вообще явно не включать тип useconds_t в ваш код. Для обеспечения

Засыпание и ожидание

413

максимальной переносимости следует действовать так, как будто этот параметр яв­
ляется unsigned int, и не полагаться на возвращаемое значение функции usleep():
void usleep (unsigned int usec);

Тогда она будет использоваться так:
unsigned int usecs = 200;
usleep (usecs);

Так мы можем работать с обоими вариантами функции, а также проверять на­
личие ошибок:
errno = 0;
usleep (1000);
if (errno)
perror ("usleep");

Но в большинстве программ такая проверка не выполняется, а ошибки usleep()
вообще игнорируются.

Засыпание с наносекундной точностью
В Linux на смену функции usleep() приходит nanosleep(), обеспечивающая разре­
шение с точностью до наносекунд и более интеллектуальный интерфейс:
#define _POSIX_C_SOURCE 199309
#include
int nanosleep (const struct timespec *req,
struct timespec *rem);

При успешном вызове функция nanosleep() переводит вызывающий процесс
в спящий режим на период, указанный в req, а потом возвращает 0. При ошибке
функция возвращает –1 и присваивает errno соответствующее значение. Если сон
процесса прерывается из-за сигнала, то вызов может вернуться прежде, чем истечет
заданное время. В таком случае функция nanosleep() возвращает –1 и присваивает
errno значение EINTR. Если rem не равно NULL, то функция записывает остаток дли­
тельности спящего режима (тот фрагмент req, который функция еще должна «про­
спать») в rem. Затем программа может повторить вызов, передав rem в качестве req
(как показано ниже в этом разделе).
Вот другие возможные в данном случае значения errno:
 EFAULT — req или rem является недопустимым указателем;
 EINVAL — одно из полей в req является недопустимым.

Как правило, работать с этой функцией очень просто:
struct timespec req = { .tv_sec = 0,
.tv_nsec = 200 };
/* засыпаем на 200 нс */

414

Глава 11. Время

ret = nanosleep (&req, NULL);
if (ret)
perror ("nanosleep");

А вот пример использования второго параметра для продолжения работы, если
сон был прерван:
struct timespec req = { .tv_sec = 0,
.tv_nsec = 1369 };
struct timespec rem;
int ret;
/* засыпаем на 1369 нс */
retry:
ret = nanosleep (&req, &rem);
if (ret) {
if (errno == EINTR) {
/* повторяем попытку, учитывая оставшийся период времени */
req.tv_sec = rem.tv_sec;
req.tv_nsec = rem.tv_nsec;
goto retry;
}
perror ("nanosleep");
}

Наконец, далее приведен альтернативный подход достижения той же цели
(пожалуй, более эффективный, но менее удобочитаемый):
struct timespec req = { .tv_sec = 1,
.tv_nsec = 0 };
struct timespec rem, *a = &req, *b = &rem;
/* засыпаем на 1 с */
while (nanosleep (a, b) && errno == EINTR) {
struct timespec *tmp = a;
a = b;
b = tmp;
}

Функция nanosleep() обладает несколькими преимуществами по сравнению с sleep()
и usleep():
 разрешение с точностью до наносекунд, а не с точностью до микросекунд или

секунд;
 стандартизирована в POSIX.1b;
 реализация не связана с использованием сигналов (недостатки чего рассмотре­

ны ниже).
Несмотря на устаревание usleep(), во многих программах эта функция предпо­
читается nanosleep(). Поскольку функция +nanosleep() стандартизирована в POSIX
и при работе не использует сигналы, новые программы должны применять ее (либо
интерфейс, рассмотренный в следующем разделе) вместо sleep() и usleep().

Засыпание и ожидание

415

Продвинутая работа со спящим режимом
Как и в случаях со всеми другими классами функций времени, рассмотренными
выше, в семействе часов POSIX имеется и очень продвинутый интерфейс для ра­
боты со спящим режимом:
#include
int clock_nanosleep (clockid_t clock_id,
int flags,
const struct timespec *req,
struct timespec *rem);

Функция clock_nanosleep() работает аналогично nanosleep(). На самом деле
следующий вызов:
ret = nanosleep (&req, &rem);

делает то же самое, что и этот:
ret = clock_nanosleep (CLOCK_REALTIME, 0, &req, &rem);

Разница заключается в параметрах clock_id и flags. Первый указывает эталон­
ный источник времени. Большинство источников времени подойдут в таком ка­
честве, хотя вы и не сможете задать процессорные часы вызывающего процесса
(например, CLOCK_PROCESS_CPUTIME_ID). Это было бы попросту бессмысленно, так как
вызов приостанавливает выполнение процесса и значение процессорного времени
перестает расти.
Выбор эталонного источника времени зависит от того, с какой целью ваша
программа переходит в спящий режим. Если она должна «проспать» до какого-то
абсолютного момента во времени, то разумнее всего выбрать CLOCK_REALTIME. Если
спящий режим должен продолжаться относительное количество времени, то иде­
альным вариантом станет CLOCK_MONOTONIC.
Параметр flags может иметь значение TIMER_ABSTIME или 0. При флаге TIMER_ABSTIME
то значение, которое указано в req, считается абсолютным, а не относительным.
Так исключается потенциальное возникновение условий гонки. Чтобы пояснить
значение этого параметра, предположим, что во время T+0 процесс собирается
«проспать» до времени T+1. В момент T+0 процесс вызывает функцию clock_gettime(),
чтобы узнать текущее время (T+0). Затем он вычитает T+0 из T+1, получая значе­
ние Y. Это значение он передает функции clock_nanosleep(). Но какое-то время
пройдет между моментом, когда было определено время, и моментом, когда
процесс заснет. Хуже, если процесс был деактивизирован процессором, обра­
тился к несуществующей странице или произошло нечто подобное. Таким об­
разом, всегда существует возможность возникновения условий гонки между
получением текущего времени, расчетом временной разницы и самим моментом
засыпания.
Флаг TIMER_ABSTIME сводит к нулю вероятность условий гонки, позволяя процес­
су прямо указать T+1. Ядро приостанавливает процесс до тех пор, пока в указанном

416

Глава 11. Время

источнике времени не наступит момент T1+1. Если время в указанном источнике
уже превышает значение T+1, то вызов сразу же возвращается.
Рассмотрим относительное и абсолютное засыпание. В следующем примере
спящий режим включается на 1,5 секунды:
struct timespec ts = { .tv_sec = 1, .tv_nsec = 500000000 };
int ret;
ret = clock_nanosleep (CLOCK_MONOTONIC, 0, &ts, NULL);
if (ret)
perror ("clock_nanosleep");

Напротив, в следующем примере период сна продолжается до абсолютного
момента во времени. Этот момент наступает ровно через одну секунду после мо­
мента времени, возвращенного функцией clock_gettime() из источника CLOCK_
MONOTONIC:
struct timespec ts;
int ret;
/* процесс собирается проспать одну секкунду с НАСТОЯЩЕГО МОМЕНТА */
ret = clock_gettime (CLOCK_MONOTONIC, &ts);
if (ret) {
perror ("clock_gettime");
return;
}
ts.tv_sec += 1;
printf ("Мы хотим заснуть до секунды=%ld наносекунды=%ld\n",
ts.tv_sec, ts.tv_nsec);
ret = clock_nanosleep (CLOCK_MONOTONIC, TIMER_ABSTIME,
&ts, NULL);
if (ret)
perror ("clock_nanosleep");

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

Переносимый способ засыпания
Вспомним функцию select(), знакомую нам из гл. 2:
#include
int select (int n,
fd_set *readfds,
fd_set *writefds,

Засыпание и ожидание

417

fd_set *exceptfds,
struct timeval *timeout);

Как было указано в той главе, функция select() обеспечивает переносимый
способ перехода в спящий режим с детализацией менее одной секунды. В течение
долгого времени для управления спящим режимом в переносимых программах
UNIX можно было пользоваться только функцией sleep(). Функция usleep() под­
держивалась далеко не везде, а nanosleep() еще даже не была написана. Разработ­
чики открыли эффективный и удобный при переносимости способ перевода про­
цессов в спящий режим: достаточно передать функции select() значения 0 для n,
NULL для всех трех указателей fd_set и желаемую длительность сна для timeout.
struct timeval tv = { .tv_sec = 0,
.tv_usec = 757 };
/* засыпание на 757 мс */
select (0, NULL, NULL, NULL, &tv);

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

Превышение пределов
Все интерфейсы, рассмотренные в этой главе, гарантируют, что спящий режим
продлится как минимум столько, сколько было запрошено (либо будет возвращена
ошибка, свидетельствующая об обратном). Такая функция никогда не возвраща­
ется успешно, прежде чем истечет заданная задержка. Но вполне возможно, что
задержка продлится дольше, чем было запрошено.
Такой феномен объясняется лишь принципом работы планировщика — возмож­
но, запрошенное на спящий режим время уже истекло, и ядро в нужный момент
«разбудило» процесс, но планировщик решил запустить какую-то другую задачу.
Но существуют и другие случаи, называемые превышением пределов таймера.
Такая ситуация может возникать, если таймер обеспечивает недостаточную степень
дискретизации и интервал между его «тиканьем» превышает запрошенный интер­
вал времени. Допустим, системный таймер «тикает» с интервалом 10 миллисекунд,
а процесс запрашивает переход в спящий режим на 1 миллисекунду. Система спо­
собна измерять время и реагировать на события, связанные с временем (например,
будить процесс), но с интервалом не менее 10 миллисекунд. Если процесс выдает
вышеупомянутый запрос на переход в спящий режим, а таймеру остается 1 мил­
лисекунда до следующего такта, то все будет хорошо — 1 миллисекунда истечет
и ядро разбудит процесс. Если же запрос на переход в спящий режим поступает от
процесса именно в тот момент, когда таймер отбивает очередной такт, то следующий
такт наступит только через 10 миллисекунд. Соответственно, процессу придется
проспать лишние 9 миллисекунд! Произойдет девять актов превышения предела,
каждый продлится по 1 микросекунде. В среднем таймер с периодом X теряет на
таких превышениях X / 2 времени.

418

Глава 11. Время

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

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

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

Простые варианты сигнализации
Функция alarm() — простейший интерфейс для работы с таймером:
#include
unsigned int alarm (unsigned int seconds);

Когда вы вызываете эту функцию, доставка сигнала SIGALRM вызывающему про­
цессу планируется на тот момент, когда истечет seconds секунд реального времени.
Если ранее назначенный сигнал находился в состоянии ожидания, то вызов отме­
няет сигнал тревоги, заменяет его новым таким запрошенным сигналом и возвра­
щает количество секунд, не истраченных предыдущим «будильником». Если зна­
чение seconds равно 0, то любой предыдущий сигнал тревоги отменяется (при его
наличии), но новый тревожный сигнал не назначается.
Таким образом, для успешного использования этой функции также требуется
зарегистрировать обработчик для сигнала SIGALRM. О сигналах и обработчиках
сигналов мы подробно говорили в предыдущей главе. Ниже приведен фрагмент

Таймеры

419

кода с функцией alarm_handler(). Здесь регистрируется обработчик сигнала SIGALRM,
длительность тревожного сигнала устанавливается равной 5 секундам:
void alarm_handler (int signum)
{
printf ("Пять секунд прошло!\n");
}
void func (void)
{
signal (SIGALRM, alarm_handler);
alarm (5);
pause ();
}

Интервальные таймеры
Системные вызовы интервальных таймеров, которые впервые появились в 4.2BSD,
с тех пор были стандартизированы в POSIX. Они обеспечивают более полный
контроль над временем, чем alarm():
#include
int getitimer (int which,
struct itimerval *value);
int setitimer (int which,
const struct itimerval *value,
struct itimerval *ovalue);

Интервальные таймеры работают подобно alarm(), но опционально их можно
настроить так, чтобы они автоматически сбрасывались и могли работать в одном
из следующих режимов.
 ITIMER_REAL — измеряет фактическое время. Когда истечет указанный период
фактического времени, ядро отправляет процессу сигнал SIGALRM.
 ITIMER_VIRTUAL — значение таймера уменьшается только в то время, когда процесс

выполняет код из пользовательского пространства. Когда указанное количество
процессорного времени истечет, ядро отправляет процессу сигнал SIGVTALRM.
 ITIMER_PROF — значение уменьшается и в ходе выполнения процесса, и в ходе

работ ядра по поручению процесса (например, при завершении системного
вызова). Когда истечет указанное количество времени, ядро отправит процессу
сигнал SIGPROF. Обычно этот режим используется в сочетании с ITIMER_VIRTUAL,
чтобы программа могла измерять пользовательское и процессное время, израс­
ходованное процессом.
 ITIMER_REAL — измеряет то же время, что и alarm(); два других режима полезны

при профилировании.

420

Глава 11. Время

В структуре itimerval пользователь может задавать количество времени, которое
должно пройти до истечения таймера. Кроме того, здесь при необходимости ука­
зывается новый срок окончания, с которым в следующий раз должен запускаться
таймер:
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};

/* следующее значение */
/* актуальное значение */

Выше мы говорили о структуре timeval, которая обеспечивает разрешение с точ­
ностью до микросекунд:
struct timeval {
long tv_sec;
long tv_usec;
};

/* секунды */
/* микросекунды */

Функция setitimer() настраивает таймер типа which, срок истечения которого
указывается в поле it_value. Как только истечет время, определенное в it_value,
ядро заново запустит таймер со значением, заданным в it_interval. Следовательно,
it_value — это время, остающееся до истечения актуального таймера. Как только
значение it_value достигает нуля, оно становится равным it_interval. Если срок
таймера истечет, а it_interval будет равно 0, то таймер не перезапустится. Анало­
гично если значение it_value активного таймера равно 0, то таймер останавлива­
ется и не перезапускается.
Если ovalue не равно NULL, то возвращаются предыдущие значения интерваль­
ного таймера типа which.
Функция getitimer() возвращает актуальные значения интервального таймера
типа which.
Обе функции возвращают 0 при успехе и –1 при ошибке, причем в последнем
случае errno присваивается одно из следующих значений:
 EFAULT — value или ovalue является недопустимым указателем;
 EINVAL — which не соответствует допустимому типу интервального таймера.

В следующем фрагменте кода создается обработчик сигнала SIGALRM (см. гл. 10),
после чего интервальный таймер запускается с первоначальным периодом истече­
ния, равным 5 секундам, и с последующим интервалом истечения, равным 1 се­
кунде:
void alarm_handler (int signo)
{
printf ("Таймер сработал!\n");
}
void foo (void)
{
struct itimerval delay;
int ret;

Таймеры

421

signal (SIGALRM, alarm_handler);
delay.it_value.tv_sec = 5;
delay.it_value.tv_usec = 0;
delay.it_interval.tv_sec = 1;
delay.it_interval.tv_usec = 0;
ret = setitimer (ITIMER_REAL, &delay, NULL);
if (ret) {
perror ("setitimer");
return;
}
pause ();
}

В некоторых системах UNIX с помощью сигнала SIGALRM реализуются вызовы
sleep() и usleep(). Функции alarm() и setitimer() также используют SIGALRM. Поэтому
программист должен внимательно следить за тем, чтобы вызовы этих функций не
перекрывали друг друга, — результат такого сочетания не определен. При необходи­
мости краткого ожидания программисту следует пользоваться функцией nanosleep(),
которая, согласно стандарту POSIX, не применяет сигналов. При работе с таймерами
следует использовать setitimer() или alarm().

Функции для расширенной работы с таймерами
Самый мощный интерфейс для работы с таймерами относится к семейству часов
POSIX.
При работе с таймерами, работа которых основана на использовании часов
POSIX, акты инстанцирования, инициализации и, наконец, удаления таймера
распределены на три самостоятельные функции: timer_create() создает таймер,
timer_settime() инициализирует таймер, а timer_delete() уничтожает его.
ПРИМЕЧАНИЕ
Семейство часов POSIX для работы с интерфейсами таймеров — несомненно, самое продвинутое,
но в то же время и самое новое (соответственно, наименее удобное при портировании) и самое
сложное в использовании. Если ваши основные приоритеты — простота и переносимость программы, то лучше всего остановиться на функции setitimer().

Создание таймера
Чтобы создать таймер, воспользуйтесь функцией timer_create():
#include
#include
int timer_create (clockid_t clockid,
struct sigevent *evp,
timer_t *timerid);

422

Глава 11. Время

При успешном вызове timer_create() создается новый таймер, ассоциированный
с часами POSIX clockid, в timerid сохраняется уникальный идентификатор време­
ни, после чего функция возвращает 0. Этот вызов просто создает нужные условия
для запуска таймера. До запуска таймера фактически ничего не происходит, в чем
вы убедитесь, прочитав следующий раздел.
В приведенном далее фрагменте кода создается новый таймер, связанный с ча­
сами POSIX CLOCK_PROCESS_CPUTIME_ID. В timer сохраняется ID таймера:
timer_t timer;
int ret;
ret = timer_create (CLOCK_PROCESS_CPUTIME_ID,
NULL,
&timer);
if (ret)
perror ("timer_create");

При ошибке вызов возвращает –1, значение timerid остается неопределенным,
а errno присваивается одно из следующих значений:
 EAGAIN — система не обладает достаточным количеством ресурсов для заверше­

ния запроса;
 EINVAL — часы POSIX, указанные в clockid, являются недопустимыми;
 ENOTSUP — часы POSIX, указанные в clockid, являются допустимыми, но система

не поддерживает использования этих часов с таймерами. Стандарт POSIX гаран­
тирует, что в любых реализациях при работе с таймерами будут поддерживаться
часы CLOCK_REALTIME. Поддержка других часов зависит от конкретной реализации.
Если значение параметра evp не равно NULL, то он определяет асинхронное уве­
домление, происходящее по истечении срока таймера. Структура задается в заго­
ловке . Предполагается, что ее содержимое должно быть скрытым от
программиста, но в ней определяются как минимум следующие поля:
#include
struct sigevent {
union sigval sigev_value;
int sigev_signo;
int sigev_notify;
void (*sigev_notify_function)(union sigval);
pthread_attr_t *sigev_notify_attributes;
};
union sigval {
int sival_int;
void *sival_ptr;
};

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

Таймеры

423

Процесс может точно указывать сигнал, который будет испускаться ядром, даже
само ядро может породить поток и выполнить функцию в ответ на истечение тай­
мера. Процесс задает поведение, наступающее при истечении времени таймера,
в поле sigev_notify, которое должно иметь одно из следующих значений:
 SIGEV_NONE — «нулевое» уведомление. При истечении таймера ничего не проис­

ходит;
 SIGEV_SIGNAL — по истечении таймера ядро направляет процессу сигнал, указан­
ный в sigev_signo. В обработчике сигналов аргументу si_value присваивается
значение sigev_value;
 SIGEV_THREAD — при истечении таймера ядро порождает новый поток (внутри
данного процесса) и приказывает ему выполнить sigev_notify_function, переда­
вая sigev_value в качестве единственного аргумента. Поток завершается, как
только возвращается от этой функции. Если sigev_notify_attributes не равно
NULL, то предоставленная структура pthread_attr_t определяет поведение нового

потока.
Если evp равно NULL, как в одном из наших предыдущих примеров, то уведомление
об истечении таймера устанавливается, как если бы sigev_notify было равно SIGEV_
SIGNAL, sigev_signo — SIGALRM, а sigev_value — ID таймера. Таким образом, по умолча­
нию эти таймеры работают во многом так же, как и интервальные таймеры POSIX.
Но стоит их немного настроить, и их возможности значительно расширятся!
В следующем примере создается таймер, работающий на основе часов CLOCK_REALTIME.
Как только срок этого таймера истечет, ядро выдаст сигнал SIGUSR1, а в качестве значе­
ния si_value установит адрес, по которому хранится ID таймера:
struct sigevent evp;
timer_t timer;
int ret;
evp.sigev_value.sival_ptr = &timer;
evp.sigev_notify = SIGEV_SIGNAL;
evp.sigev_signo = SIGUSR1;
ret = timer_create (CLOCK_REALTIME,
&evp,
&timer);
if (ret)
perror ("timer_create");

Установка таймера
Таймер, создаваемый с помощью timer_create(), еще не установлен ни на какое
конкретное время. Чтобы ассоциировать его со сроком истечения и запустить часы,
используйте timer_settime():
#include
int timer_settime (timer_t timerid,
int flags,

424

Глава 11. Время

const struct itimerspec *value,
struct itimerspec *ovalue);

При успешном вызове timer_settime() начаинает работать таймер, указанный
с помощью timerid, со сроком истечения value, представляющий собой структуру
itimerspec:
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
}

/* следующее значение */
/* актуальное значение */

Как и в случае с setitimer(), it_value указывает актуальный срок истечения
таймера. Когда таймер доходит до конца своего интервала, поле it_value обновля­
ется значением из поля it_interval. Если it_interval равно 0, то таймер не явля­
ется интервальным, поэтому прекращает работать, как только закончится срок
it_value.
Выше мы обсуждали структуру timespec, обеспечивающую разрешение с точно­
стью до наносекунд:
struct timespec {
time_t tv_sec;
long tv_nsec;
};

/* секунды */
/* наносекунды */

Если параметр flags равен TIMER_ABSTIME, то время, указанное в value, интерпре­
тируется как абсолютное (напротив, по умолчанию это значение отсчитывается
относительно текущего времени). Такое модифицированное поведение позволяет
избежать условий гонки на этапах получения текущего времени, расчета относи­
тельной разницы между этим значением времени и целевым моментом в будущем.
Подробнее этот вопрос обсуждается в подразд. «Продвинутая работа со спящим
режимом» разд. «Засыпание и ожидание» этой главы.
Если значение ovalue не равно NULL, то предыдущий срок истечения таймера
сохраняется в itimerspec. Если ранее таймер был отключен, то всем членам струк­
туры присваивается 0.
С помощью значения timer, ранее инициализированного в timer_create(), в сле­
дующем примере создается периодический таймер, срок которого истекает раз в се­
кунду:
struct itimerspec ts;
int ret;
ts.it_interval.tv_sec = 1;
ts.it_interval.tv_nsec = 0;
ts.it_value.tv_sec = 1;
ts.it_value.tv_nsec = 0;
ret = timer_settime (timer, 0, &ts, NULL);
if (ret)
perror ("timer_settime");

Таймеры

425

Получение времени истечения таймера
Можно узнать срок истечения таймера, не сбрасывая его с помощью timer_gettime():
#include
int timer_gettime (timer_t timerid,
struct itimerspec *value);

Успешный вызов timer_gettime() сохраняет время истечения того таймера, ко­
торый указан с помощью аргумента timerid, в структуре, на которую указывает
value, после чего функция возвращает 0. При ошибке вызов возвращает –1 и при­
сваивает переменной errno одно из следующих значений:
 EFAULT — value является недопустимым указателем;
 EINVAL — timerid является недопустимым таймером.

Например:
struct itimerspec ts;
int ret;
ret = timer_gettime (timer, &ts);
if (ret)
perror ("timer_gettime");
else {
printf ("текущее время истечения: секунды=%ld наносекунды=%ld\n",
ts.it_value.tv_sec, ts.it_value.tv_nsec);
printf ("следующее время истечения: секунды=%ld наносекунды=%ld\n",
ts.it_interval.tv_sec, ts.it_interval.tv_nsec);
}

Получение превышения пределов таймера
Стандарт POSIX содержит интерфейс для определения количества превышений
пределов, возникших у конкретного таймера, — если такие превышения имели
место:
#include
int timer_getoverrun (timer_t timerid);

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

426

Глава 11. Время

у определенного таймера превышает DELAYTIMER_MAX, он возвращается к нулю и на­
чинает работать заново.
При ошибке эта функция возвращает –1 и присваивает errno значение EINVAL.
Это единственно возможное условие ошибки, указывающее, что таймер, заданный
в timerid, является недопустимым.
Например:
int ret;
ret = timer_getoverrun (timer);
if (ret == –1)
perror ("timer_getoverrun");
else if (ret == 0)
printf ("превышений не было\n");
else
printf ("%d превышений\n", ret);

Удаление таймера
Удалить таймер просто:
#include
int timer_delete (timer_t timerid);

Успешный вызов функции timer_delete() уничтожает таймер, ассоциированный
с timerid, и возвращает 0. При ошибке вызов возвращает –1 и присваивает errno
значение EINVAL. Это единственно возможное условие ошибки, указывающее, что
таймер, заданный в timerid, является недопустимым.

Приложение A
Расширения GCC
для языка C
В наборе компиляторов GNU (DNU Compiler Connection, GCC) содержится мно­
жество расширений для языка C, многие из которых зарекомендовали себя как
очень ценные инструменты системных программистов. Большинство дополнений
к языку C, которые мы рассмотрим в этом приложении, сообщают компилятору
дополнительную информацию о поведении и предполагаемом назначении их кода.
Компилятор, в свою очередь, использует эту информацию, чтобы генерировать
более эффективный машинный код. Другие расширения заполняют пробелы, су­
ществующие в языке C, — особенно на низком уровне.
GCC предоставляет несколько расширений, в настоящее время входящих в но­
вейший стандарт языка C, ISO C11. Одни из этих расширений работают аналогич­
но соответствующим функциям из C11, но другие расширения реализованы в ISO
C11 существенно иначе. Новый код должен представлять собой стандартизирован­
ные разновидности этих возможностей. Мы не будем рассматривать здесь такие
расширения, а коснемся лишь тех, которые встречаются только в GCC.

GNU C
Вариант языка C, поддерживаемый GCC, часто именуется GNU C. В 1990-е годы
GNU C заполнил множество пробелов, существовавших в языке C, и предоставил
такие компоненты, как сложные переменные, массивы нулевой длины, встраиваемые
функции и именованные инициализаторы. Но спустя примерно десятилетие язык C
наконец был усовершенствован, а после появления стандартов ISO C99 и ISO C11
расширения GCC стали менее востребованы. Тем не менее GNU C по-прежнему
содержит немало полезных возможностей, а многие программисты, пишущие на
Linux, продолжают использовать поднабор GNU C — иногда всего по одному-два
расширения — в своем коде, соответствующем стандартам C99 или C11.
Одна из наиболее известных баз кода, основанных на GCC, — это само ядро
Linux, написанное только на GNU C. Правда, не так давно компания Intel присту­
пила к разработкам, которые позволили бы компилятору Intel C Compiler (ICC)
понимать расширения GNU C, используемые ядром. Следовательно, многие из
этих расширений сегодня утрачивают свою GCC-специфичность.

428

Приложение A. Расширения GCC для языка C

Встраиваемые функции
Компилятор копирует весь код встраиваемой функции в то место, где эта функция
вызывается. Вместо того чтобы хранить функцию внешне и переходить к ней
всякий раз при ее вызове, компилятор выполняет содержимое такой функции
безотрывно от остального кода. Благодаря такому поведению удается избавиться
от издержек, связанных с вызовом функции. Кроме того, открываются потенци­
альные возможности для оптимизаций в точке вызова, так как компилятор может
одновременно корректировать работу и вызывающей и вызываемой стороны.
Последнее особенно актуально, если параметры, передаваемые функции на сто­
роне вызова, постоянны. Однако при копировании функции во все мельчайшие
фрагменты кода, где она вызывается, код может увеличиться до совершенно не­
допустимых размеров. Следовательно, функции следует встраивать лишь в тех
случаях, когда они маленькие и простые либо вызываются в ограниченном коли­
честве мест.
На протяжении многих лет GCC поддерживал ключевое слово inline, приказы­
вающее компилятору встроить данную функцию. В C99 это ключевое слово было
формализовано:
static inline int foo (void) { /* ... */ }

Однако на практике оно представляет собой не более чем подсказку — напоми­
нание компилятору о том, чтобы он рассмотрел возможность встраивания данной
функции. GCC дополнительно предоставляет расширение, приказывающее ком­
пилятору всегда встраивать указанную функцию:
static inline __attribute__ ((always_inline)) int foo (void) { /* ... */ }

Наиболее очевидные кандидаты на роль встраиваемых функций — препроцес­
сорные макрокоманды. Встраиваемая функция в GCC будет вести себя точно так
же, как и макрокоманда, а также получать проверку типов. Например, вместо такой
макрокоманды
#define max(a,b) ({ a > b ? a : b; })

можно использовать следующую встраиваемую функцию:
static inline max (int a, int b)
{
if (a > b)
return a;
return b;
}

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

Чистые функции

429

Подавление встраивания
Когда GCC работает в наиболее агрессивном режиме оптимизации, он автомати­
чески выбирает подходящие функции и встраивает их. Как правило, такой подход
себя оправдывает, но иногда программист точно знает, что функция будет работать
неправильно, если ее встроить. В частности, это происходит при использовании
__builtin_return_address (рассматривается далее в этом приложении). Для подав­
ления встраивания применяйте ключевое слово noinline:
__attribute__ ((noinline)) int foo (void) { /* ... */ }

Чистые функции
Чистой называется функция, которая не оказывает побочных эффектов и чье воз­
вращаемое значение отражает только параметры функции или неизменчивые
глобальные переменные. Любой доступ к параметрам или глобальным переменным
должен быть открыт только для чтения. С такими функциями может применяться
оптимизация циклов и удаление подвыражений. Функции помечаются как чистые
с помощью ключевого слова pure:
__attribute__ ((pure)) int foo (int val) { /* ... */ }

Классический пример такой функции — strlen(). При наличии идентичных
образцов ввода возвращаемое значение этой функции инвариантно для множества
вызовов. Поэтому ее можно вычленить из цикла и вызвать всего однажды. Рассмот­
рим следующий код:
/* символ за символом записываем каждую букву в 'p' в верхнем регистре */
for (i = 0; i < strlen (p); i++)
printf ("%c", toupper (p[i]));

Если компилятору неизвестно, что функция strlen() является чистой, то ему
придется вызывать ее в каждой итерации цикла.
Толковый программист — а также компилятор, знающий, что функция strlen()
помечена как чистая, — напишет (сгенерирует) такой код:
size_t len;
len = strlen (p);
for (i = 0; i < len; i++)
printf ("%c", toupper (p[i]));

Между прочим, особо умные программисты (такие, как читатели этой книги)
напишут так:
while (*p)
printf ("%c", toupper (*p++));

430

Приложение A. Расширения GCC для языка C

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

Постоянные функции
Постоянная функция — ужесточенный вариант чистой. Такие функции не могут
обращаться к глобальным переменным и принимать указатели в качестве парамет­
ров. Следовательно, возвращаемое значение постоянной функции не отражает
ничего, кроме параметров, переданных по значению. С этими функциями могут
выполняться дополнительные виды оптимизаций — кроме тех, что применимы
с чистыми функциями. К постоянным относятся, например, такие чистые функции,
как abs() (предполагается, что они не сохраняют состояния и не выкидывают дру­
гих фокусов ради оптимизации). Программист помечает функцию как постоянную
с помощью ключевого слова const:
__attribute__ ((const)) int foo (int val) { /* ... */ }

Как и в случае с чистыми функциями, нет смысла, чтобы постоянная функция
возвращала значение типа void.

Невозвращаемые функции
Если функция не возвращает результат — например, потому, что она инвариантно
вызывает exit(), — то программист может пометить ее ключевым словом noreturn,
подчеркнув этот факт для компилятора:
__attribute__ ((noreturn)) void foo (int val) { /* ... */ }

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

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

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

Как пометить функцию как используемую

431

функция сама для себя выделяет свежий фрагмент памяти и возвращает указатель
на него, — то программист может пометить такую функцию ключевым словом
malloc, а компилятор, в свою очередь, выполнит соответствующую оптимизацию:
__attribute__ ((malloc)) void * get_page (void)
{
int page_size;
page_size = getpagesize ();
if (page_size _b ? _a : _b; \
})

Получение границы выравнивания типа
GCC предоставляет ключевое слово alignof, позволяющее определить вырав­
нивание заданного объекта. Это значение зависит от конкретной архитектуры
и ABI. Если в актуальной архитектуре не обеспечивается требуемое выравни­
вание, то ключевое слово возвращает выравнивание, рекомендуемое в данном
ABI. В противном случае ключевое слово возвращает минимальное требуемое
выравнивание.
Синтаксис идентичен синтаксису sizeof():
__alignof__(int)

В зависимости от архитектуры данная функция, вероятно, вернет 4, так как
32-битные целые числа обычно выравниваются по 4-байтным границам.
ПРИМЕЧАНИЕ
В C11 и C++11 появилась функция alignof(), которая действует идентично alignof(), но при этом
стандартизирована. При написании программ на C11 или C++11 следует отдавать предпочтение
alignof().

Это ключевое слово работает и с адресами переменных (lvalue). В данном слу­
чае возвращается минимальное выравнивание базового типа, а не актуальное вы­
равнивание конкретного адреса переменной. Если минимальное выравнивание
было изменено с помощью атрибута aligned (описано выше, в разд. «Увеличение
границы выравнивания переменной» этогоприложения), то это изменение отра­
жается ключевым словом __alignof__.
Например, рассмотрим следующую структуру:
struct ship {
int year_built;
char cannons;
int mast_height;
};

а также этот фрагмент кода:
struct ship my_ship;
printf ("%d\n", __alignof__(my_ship.cannons));

Получение возвращаемого адреса функции

437

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

Смещение члена внутри структуры
GCC предоставляет встроенное ключевое слово для определения смещения члена
структуры внутри структуры. Макрос offsetof(), определенный в , входит
в состав стандарта ISO C. Большинство определений просто ужасны, они содержат
непристойную арифметику над указателями и не рекомендуются несовершенно­
летним читателям. Расширение из GCC проще, а также потенциально быстрее:
#define offsetof(type, member) __builtin_offsetof (type, member)

Вызов возвращает смещение члена member внутри типа, то есть количество бай­
тов от нуля (с начала этой структуры) до данного члена. Расмотрим, например,
следующую структуру:
struct rowboat {
char *boat_name;
unsigned int nr_oars;
short length;
};

Точные значения смещений зависят от размера переменных, требований, предъ­
являемых к выравниванию в данной архитектуре, а также от принципа заполнения.
На 32-битной машине мы вполне можем встретить вызов offsetof() в структуре
rowboat с полями boat_name, nr_oars и length, чьи возвращаемые значения будут
равны 0, 4 и 8 соответственно.
В системе Linux макрос offsetof() должен определяться с применением ключе­
вого слова из GCC. Повторно определять его не требуется.

Получение возвращаемого адреса функции
GCC предоставляет ключевое слово для получения возвращаемого адреса актуаль­
ной функции либо одного из «вызывателей» этой функции:
void * __builtin_return_address (unsigned int level)

Параметр level указывает в цепочке вызовов ту функцию, адрес которой должен
быть возвращен. Значение 0 требует возвращаемый адрес актуальной функции,
значение 1 — адрес той функции, которая вызвала актуальную, значение 2 — функ­
цию, вызвавшую ту, которой соответствует значение 1, и т. д.
Если актуальная функция является встроенной, то возвращается адрес вызы­
вающей функции. Если это неприемлемо, то пользуйтесь ключевым словом noinline.
Так вы принудите компилятор не встраивать функции.
Существует несколько способов применения ключевого слова __builtin_
return_address. В частности, оно используется в отладочных и справочных целях.

438

Приложение A. Расширения GCC для языка C

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

Диапазоны оператора case
GCC позволяет указывать в метках оператора case диапазон значений для отдель­
но взятого блока. Синтаксис таков:
case low ... high:

Например:
switch (val) {
case 1 ... 10:
/* ... */
break;
case 11 ... 20:
/* ... */
break;
default:
/* ... */
}

Эта функциональность очень полезна при работе с диапазоном значений
ASCII:
case 'A' ... 'Z':

Обратите внимание, что до и после многоточия должны быть пробелы. В про­
тивном случае компилятор может «запутаться», особенно в целочисленных диапа­
зонах. Всегда делайте так:
case 4 ... 8:

а так никогда не делайте:
case 4...8:

Арифметика указателей типа void
и указателей на функции
В GCC операции сложения и вычитания допускаются над указателями типа void
и над указателями на функции. Как правило, стандарт ISO C не позволяет выполнять

439

Более переносимо и красиво

арифметические операции над такими указателями, поскольку размер значения
типа void — это фактически нонсенс, он зависит от того, на какую именно сущность
направлен указатель. Для обеспечения подобной арифметики GCC принимает
размер такого адресуемого объекта за один байт. Следующий фрагмент кода уве­
личивает значение a на единицу:
a++; /* a — это void-указатель */

Параметр -Wpointer-arith заставляет GCC генерировать предупреждение, когда
используются такие расширения.

Более переносимо и красиво
Согласитесь, синтаксис attribute очень некрасивый. Некоторые расширения, рас­
смотренные в этом приложении, требуют использования препроцессорных макро­
сов — только так работа с ними становится терпимой. Но любые расширения лишь
выиграют, если немного улучшить их внешний вид.
Немного препроцессорных уловок — и все получается как по волшебству. Более
того, выполняя такую работу, мы также обеспечиваем портируемость GCC-расши­
рений, определяя их таким образом, что они будут понятны любому компилятору
(а не только GCC).
Для этого включите следующий фрагмент кода в заголовок и добавьте этот
заголовок в ваши файлы исходников:
#if __GNUC_ _ >= 3
# undef inline
# define inline inline
# define __noinline
# define __pure
# define __const
# define __noreturn
# define __malloc
# define __must_check
# define __deprecated
# define __used
# define __unused
# define __packed
# define __align(x)
# define __align_max
# define likely(x)
# define unlikely(x)
#else
# define __noinline
# define __pure
# define __const
# define __noreturn
# define __malloc
# define __must_check

__attribute_ _ ((always_inline))
__attribute__ ((noinline))
__attribute__ ((pure))
__attribute__ ((const))
__attribute__ ((noreturn))
__attribute__ ((malloc))
__attribute__ ((warn_unused_result))
__attribute__ ((deprecated))
__attribute__ ((used))
__attribute__ ((unused))
__attribute__ ((packed))
__attribute__ ((aligned (x)))
__attribute__ ((aligned))
__builtin_expect (!!(x), 1)
__builtin_expect (!!(x), 0)
/*
/*
/*
/*
/*
/*

не
не
не
не
не
не

noinline */
pure */
const */
noreturn */
malloc */
warn_unused_result */

440
# define
# define
# define
# define
# define
# define
# define
# define
#endif

Приложение A. Расширения GCC для языка C

__deprecated /*
__used
/*
__unused
/*
__packed
/*
__align(x)
/*
__align_max
/*
likely(x) (x)
unlikely(x) (x)

не
не
не
не
не
не

deprecated */
used */
unused */
packed */
aligned */
align_max */

Например, в следующем фрагменте мы помечаем эту функцию как чистую,
пользуясь одним из вышеперечисленных сокращений:
__pure int foo (void) { /* ... */ }

Если применяется GCC, то эта функция помечается атрибутом pure. Если мы
работаем не с GCC, а с другим компилятором, то препроцессор однозначно требу­
ет метку __pure. Обратите внимание: в каждом определении вы можете указывать
несколько атрибутов и соответственно одновременно применять несколько из
вышеперечисленных комбинаций.
Проще, красивее, портабельнее!

Приложение Б
Библиография
В этом приложении перечислена рекомендуемая литература, имеющая отношение
к системному программированию. Все приведенные источники разбиты на четы­
ре подкатегории. Ни одна из этих работ не является обязательной к прочтению.
Это просто моя подборка книг, которые кажутся мне наиболее ценными материа­
лами по той или иной теме. Если вы желаете ознакомиться с дополнительной
информацией, расширяющей материал моей книги, то именно эти книги я бы вам
посоветовал.
Некоторые работы из данной подборки посвящены темам, в которых вы уже
должны хорошо ориентироваться, — например, языку C. Другие тексты значитель­
но дополняют книгу, подробно рассказывая, в частности, о gdb, Git и о проектиро­
вании операционных систем. Разумеется, данный список неполный — наверняка
вы найдете и другие, не менее ценные ресурсы.

Книги по языку
программирования C
В книгах из этого раздела рассматривается язык C — лингва франка1 системного
программирования. Если вы не умеете писать код на C столь же непринужденно,
как говорить на родном языке, то одна или несколько следующих книг (а также
большая практическая работа) помогут вам улучшить знания этого языка. Особо
рекомендую изучить первую книгу, иногда сокращенно именуемую K & R. Ее
краткость красноречиво показывает, насколько прост язык C.
 Керниган Б., Ритчи Д. Язык программирования Си (The C programming language). —

2-е изд. — М.: Вильямс, 2007.

1

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

442

Приложение Б. Библиография

Эта книга написана одним из авторов языка C и его соратником. Представляет
собой наиболее фундаментальную работу по языку C.
 Prinz Peter, Crawford Tony. C in a Nutshell. — O'Reilly Media, 2005.

Отличная книга, рассказывающая как о языке C, так и о стандартной библио­
теке C.
 Prinz Peter, Kirch-Prinz Ulla. C Pocket Reference. — O'Reilly Media, 2002.

Краткий справочник по языку C, обновленный с учетом ANSI C99.
 Van der Linden Peter. Expert C Programming. — Prentice Hall, 1994.

Прекрасная книга о сравнительно малоизвестных аспектах языка C, написанная
в остроумном стиле и сдобренная авторским юмором.
 Summit Steve. C Programming FAQs: Frequently Asked Questions. — 2nd edition. —

Addison-Wesley, 1995.
Эта книга — настоящий шедевр. Она содержит более 400 часто задаваемых
вопросов (с ответами) по языку программирования C. Ответы на многие из
вопросов покажутся банальными знатокам языка C, но некоторые глубокие
вопросы и ответы впечатлят даже самых эрудированных C-программистов.
У этой книги есть и интернет-версия, которая обновляется гораздо оперативнее,
чем бумажная.

Книги по программированию в Linux
Следующие тексты посвящены программированию в Linux. В них рассматривают­
ся некоторые темы, не затронутые в этой книге, а также многие интересные ин­
струменты для программирования.
 Стивенс У., Феннер Б., Рудофф Э. UNIX: Разработка сетевых приложений. —

3-е изд. — СПб.: Питер, 2004.
Исчерпывающая работа, посвященная API сокетов. К сожалению, книга расска­
зывает не только о Linux, однако радует, что она была обновлена с учетом стан­
дарта IPv6.
 Стивенс У. UNIX: взаимодействие процессов. — СПб.: Питер, 2001.

Отличная книга, посвященная межпроцессной коммуникации (IPC).
 Nichols Bradford. PThreads Programming. — O'Reilly Media, 1996.

Обзор интерфейса прикладного программирования потоков в POSIX.
 Mecklenburg Robert. Managing Projects with GNU Make. — O'Reilly Media, 2004.

В этой книге отлично рассмотрен GNU Make — классический инструмент для
выстраивания программных проектов в Linux.
 Collins-Sussman Ben. Version Control with Subversion. — O'Reilly Media, 2004.

Исчерпывающая работа о Subversion — системе контроля версий, пришедшей
на смену CVS. В UNIX эта система применяется как для контроля версий,

Книги об организации операционных систем

443

так и для управления исходным кодом. Книга написана тремя авторами
Subversion.
 Loeliger Jon. Version Control with Git. — O'Reilly Media, 2012.

Великолепное введение в работу с Git — иногда немного запутанной, но неиз­
менно мощной распределенной системой контроля версий.
 Robbins Arnold. GDB Pocket Reference. — O'Reilly Media, 2005.

Удобный карманный справочник по gdb — отладчику для Linux.
 Siever Ellen. Linux in a Nutshell. — O'Reilly Media, 2009.

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

Книги, посвященные ядру Linux
Две книги, упомянутые в этом разделе, рассказывают о ядре Linux. Существует три
основные причины, по которым следует подробнее познакомиться с этой темой.
Во-первых, ядро предоставляет интерфейс для взаимодействия между системными
вызовами и программами из пользовательского пространства, поэтому оно имеет
ключевое значение для системного программирования. Во-вторых, книга проливает
свет на различные особенности поведения и странности, возникающие при взаи­
модействии ядра с приложением. Наконец, ядро Linux — образец отличного кода,
и эти книги просто интересны.
 Лав Р. Разработка ядра Linux. — 2-е изд. — М.: Вильямс, 2006.

Это моя работа, относящаяся к данной категории. Она отлично подойдет си­
стемным программистам, желающим разобраться в конструкции и реализации
ядра Linux. Эта книга не является справочником по API. В ней подробно обсу­
ждаются алгоритмы, применяемые в ядре Linux, рассматриваются принципы
принятия решений в ядре.
 Corbet Jonathan. Linux Device Drivers. — O'Reilly Media, 2005.

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

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

444

Приложение Б. Библиография

глубокое понимание операционной системы, в которой вы программируете, зна­
чительно повышает качество создаваемого кода.
 Silberschatz Abraham. Operating System Concepts. — Prentice Hall, 2012.

Отличное введение в тему операционных систем, их историю и базовые алго­
ритмы. В книге содержится хороший набор ситуативных исследований.
 Schimmel Curt. UNIX Systems for Modern Architectures. — Addison-Wesley,

1994.
Эта книга рассказывает не столько об UNIX, сколько о современнных процес­
сорных и кэш-архитектурах, является отличным введением в тему взаимодей­
ствия операционных систем и аппаратного обеспечения. Хотя книга немного
устарела, я настоятельно ее рекомендую.

Р. Лав

Linux. Системное программирование
2-е издание
Перевел с английского О. Сивченко

Заведующий редакцией
Д. Виницкий
Ведущий редактор
Н. Гринчик
Литературный редактор
В. Конаш
Художник
Л. Адуевская
Корректоры
Т. Курьянович, Е. Павлович
Верстка
А. Барцевич

ООО «Питер Пресс», 192102, Санкт-Петербург, ул. Андреевская (д. Волкова), 3, литер А, пом. 7Н.
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная.
Подписано в печать 27.03.14. Формат 70×100/16. Усл. п. л. 36,120. Тираж 1500. Заказ 0000.
Отпечатано в полном соответствии с качеством предоставленных издательством материалов
в ГППО «Псковская областная типография». 180004, Псков, ул. Ротная, 34.

ЗАКАЗАТЬ КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР» МОЖНО ЛЮБЫМ
УДОБНЫМ ДЛЯ ВАС СПОСОБОМ:
на нашем сайте: www.piter.com
по электронной почте: postbook@piter.com
по телефону: (812) 703‹73‹74
по почте: 197198, Санкт‹Петербург, а/я 127, ООО «Питер Мейл»
по ICQ: 413763617

ВЫ МОЖЕТЕ ВЫБРАТЬ ЛЮБОЙ УДОБНЫЙ ДЛЯ ВАС СПОСОБ ОПЛАТЫ:
Наложенным платежом с оплатой при получении в ближайшем почтовом
отделении.
С помощью банковской карты. Во время заказа Вы будете перенаправлены
на защищенный сервер нашего оператора, где сможете ввести свои данные
для оплаты.
Электронными деньгами. Мы принимаем к оплате все виды электронных денег: от традиционных Яндекс.Деньги и Web-money до USD
E-Gold, MoneyMail, INOCard, RBK Money (RuPay), USD Bets, Mobile Wallet
и др.
В любом банке, распечатав квитанцию, которая формируется автоматически
после совершения Вами заказа.
Все посылки отправляются через «Почту России». Отработанная
система позволяет нам организовывать доставку Ваших покупок максимально быстро. Дату отправления Вашей покупки и предполагаемую дату
доставки Вам сообщат по e-mail.

ПРИ ОФОРМЛЕНИИ ЗАКАЗА УКАЖИТЕ:
фамилию, имя, отчество, телефон, факс, e mail;
почтовый индекс, регион, район, населенный пункт, улицу, дом,
корпус, квартиру;
название книги, автора, количество заказываемых экземпляров.