82: poll(pollFds, numFds, 0);
83: count++;
84: }
85:
86: printf("Вызовов poll() в секунду: %d\n", count);
87:
88: alarm(1);
89:
90: count = 0;
91: gotAlarm = 0;
92: alarm(1);
93: while (!gotAlarm) {
94: epoll_wait(epfd, &event, 1, 0);
95: count++;
96: }
97:
98: printf("Вызовов epoll() в секунду: %d\n", count);
99:
100: return 0;
101: }
13.2. Отображение в памяти
Операционная система Linux позволяет процессу отображать файлы в их адресное пространство. Такое отображение создает взаимно однозначное соответствие между данными в файле и в отображаемой области памяти. Отображение в памяти обладает рядом преимуществ.
Высокоскоростной доступ к файлам. Нормальные механизмы ввода-вывода, такие как read() и write(), вынуждают ядро копировать данные через буфер ядра, а не непосредственно между файлом, содержащим устройство, и процессом пространства пользователя. Карты памяти устраняют этот промежуточный буфер, сохраняя копию памяти[84].
Исполняемые файлы можно отображать на память программы, позволяя программе динамически загружать новые исполняемые области. Именно так реализуется динамическая загрузка, описанная в главе 27.
Новую память можно распределить отображением части /dev/zero, специального устройства, состоящего из нулей[85], или же через анонимное отображение. Средство Electric Fence, описанное в главе 7, использует этот механизм для распределения памяти.
Новую память, распределенную посредством карт памяти, можно сделать исполняемой, наполняя ее машинными командами, которые затем запускаются. Это свойство используется оперативными (just-in-time) компиляторами.
Файлы могут рассматриваться как память и читаться с использованием указателей, а не системных вызовов. Это существенно упрощает программы, избавляя от необходимости применения вызовов read(), write() и seek().
Отображение в памяти позволяет процессам совместно использовать области памяти, участвующие в создании и уничтожении процесса. Содержимое памяти хранится в отображаемом файле, делая его независимым от процессов.
13.2.1. Выравнивание по страницам
Системная память делится на порции под названием страницы. Размер страницы изменяется в зависимости от архитектуры, и на некоторых процессорах размер страницы может изменяться ядром. Функция getpagesize() возвращает размер (в байтах) каждой страницы системы.
#include <unistd.h>
size_t getpagesize(void);
Для каждой страницы системы ядро сообщает оборудованию, каким образом каждый процесс может получить доступ к странице (например, записать, выполнить или не выполнять никаких действий). Когда процесс пытается получить доступ к странице способом, нарушающим ограничения ядра, это вызывает ошибку сегментации (SIGSEGV), которая обычно приводит к завершению процесса.
Адрес памяти должен быть выровнен по страницам, если это адрес начала страницы. Иначе говоря, адрес должен быть целым, кратным размеру страницы архитектуры. В системе со страницами в 4 Кбайт адреса 0, 4 096, 16 384 и 32 768 являются выровненными по страницам (конечно, это далеко не весь список), потому что первая, вторая, пятая и девятая страницы системы начинаются с указанных адресов.
13.2.2. Установка отображения в памяти
Новые карты памяти создаются с помощью системного вызова mmap().
#include <sys/mman.h>
caddr_tmmap(caddr_t address, size_t length , int protection, int flags,
int fd, off_t offset);
Параметр address указывает, где именно в памяти необходимо отображать данные. Обычно address — это NULL, который означает, что для процесса не имеет значения местонахождение новой карты, и позволяет ядру выбрать любой адрес. Если адрес указан, он должен быть выровнен по страницам и в данный момент не использоваться. Если запрашиваемая карта будет конфликтовать с другой картой или не будет выровнена по страницам, mmap() может дать сбой.
Второй параметр, length, сообщает ядру, какую часть файлов следует отображать в памяти. Можно успешно отобразить больше памяти, чем количество данных в наличии у файла, но попытка доступа к нему может привести к SIGSEGV[86].
Процесс проверяет, какие типы доступа разрешены новой области памяти. Это должно быть одно или несколько значений из табл. 13.2, объединенных с помощью битового "ИЛИ", либо PROT_NONE, если доступ к отображаемой области запрещен. Файл может отображаться только для типов доступа, которые также были запрошены при изначальном открытии файла. Например, файл, открытый как O_RDONLY, не может быть отображен для записи с помощью PROT_WRITE.
Таблица 13.2. Флаги защиты mmap()
| Флаг | Описание |
|---|---|
PROT_READ |
Из отображаемой области можно читать. |
PROT_WRITE |
В отображаемую область можно записывать. |
PROT_EXEC |
Отображаемую область можно выполнять. |
Принудительное применение определенной защиты ограничено аппаратной платформой, на которой работает программа. Во многих архитектурах не разрешено выполнение кода в области памяти, если из нее запрещено чтение. При таком оборудовании отображение области с помощью PROT_EXEC эквивалентно ее отображению с помощью PROT_EXEC | PROT_READ.
По этой причине на флаги защиты памяти, передаваемые в mmap(), следует полагаться лишь как на обеспечивающие минимальную защиту.
В flags определяются другие атрибуты отображаемой области. В табл. 13.3 описаны все флаги. Многие флаги, поддерживаемые Linux, нестандартны, но могут быть полезны при особых условиях. В табл. 13.3 приведены различия между стандартными флагами mmap() и дополнительными флагами Linux. Во всех вызовах mmap() должен быть специфицирован MAP_PRIVATE или MAP_SHARED; остальные флаги устанавливать необязательно.
Таблица 13.3. Флаги mmap()
| Флаг | POSIX? | Описание |
|---|---|---|
MAP_ANONYMOUS |
Да | Игнорировать fd, создать анонимную карту. |
MAP_FIXED |
Да | Сбой в случае недопустимого адреса (address). |
MAP_PRIVATE |
Да | Запись приватна для процесса. |
MAP_SHARED |
Да | Запись копируется в файл. |
MAP_DENYWRIТЕ |
Нет | Не разрешать нормальную запись в файл. |
MAP_GROWSDOWN |
Нет | Расширить область памяти сверху вниз. |
MAP_LOCKED |
Нет | Блокировать страницы в памяти. |
84
Сохранение копии памяти может показаться не столь важным, но благодаря эффективному механизму кэширования Linux, эти задержки копий являются самой медленной частью записи в файлы данных, в которых нет набора O_SYNC.
85
Хотя большинство устройств символьного ввода-вывода не могут быть отображены, /dev/zero отображается именно для этого типа приложений.