Программирование для Linux. Профессиональный подход.
Глава 8. Системные вызовы Linux.
Мы уже познакомились с большим количеством функций, реализующих различные системные задачи, например анализ командной строки, манипулирование процессами и отображение файлов в памяти. Если присмотреться повнимательнее, то окажется, что все они подпадают под две категории в зависимости от способа реализации.
■ Библиотечная функция — это обычная функция, которая находится во внешней библиотеке, подключаемой к программе. Большинство рассмотренных нами функций содержится в стандартной библиотеке языка С, libc. Вызов библиотечной функции реализуется традиционно: ее аргументы помещаются в регистры процессора или в стек и управление передается в начало кода функции (этот код находится в библиотеке, загруженной в память).
■ Системный вызов реализован в ядре Linux. Аргументы вызова упаковываются и передаются ядру, которое берет на себя управление программой, пока вызов не завершится. Системный вызов — это не обычная функция, и для передачи управления ядру требуется специальная подпрограмма. В GNU-библнотеке языка С (реализация стандартной библиотеки, имеющаяся в Linux) для системных вызовов созданы функции-оболочки, упрощающие обращение к ним. В качестве примеров системных вызовов можно привести низкоуровневые функции ввода-вывода, такие как open() и read().
Совокупность системных вызовов Linux формирует основной интерфейс между программами и ядром. Каждому вызову соответствует некая элементарная операция или функция.
Некоторые системные вызовы оказывают очень большое влияние на систему. Например. есть вызовы, позволяющие завершить работу Linux, выделить системные ресурсы или запретить другим пользователям доступ к ресурсам. С такими вызовами связано ограничение: только процессы, выполняющиеся с привилегиями суперпользователя (учетная запись root), имеют право обращаться к ним. В противном случае вызовы завершатся ошибкой.
Внутри себя библиотечная функция может обращаться к другим функциям или системным вызовам.
В настоящее время в Linux есть около 200 системных вызовов. Их список находится в файле /usr/include/asm/unistd.h. Некоторые из них используются только внутри системы, а некоторые предназначены лишь для реализации специализированных библиотечных функций. В этой главе будут рассмотрены те системные вызовы, которые чаще всего используются системными программистами.
8.1. Команда strace.
Прежде чем изучать системные вызовы, полезно познакомиться с командой strace, которая отслеживает выполнение заданной программы, выводя список всех запрашиваемых системных вызовов и получаемых сигналов. Эта команда ставится в начале строки вызова программы, например:[26].
% strace hostname.
В результате будет получено несколько экранов выходной информации. Каждая строка соответствует одному системному вызову. В строке указываются имя вызова, его аргументы (или их сокращенные обозначения, если аргументы слишком длинные) и возвращаемое значение. По возможности команда strace старается отображать не числовые значения, а символические константы. Показываются также поля структур, переданных по указателю. Вызовы обычных функций не регистрируются.
В случае команды strace hostname первая строка сообщает о системном вызове execve(), загружающем программу hostname:[27].
Execve("/bin/hostname", ["hostname"], [/* 49 vars */]) = 0.
Первый аргумент — это имя запускаемой программы. За ним идет список аргументов, состоящий из одного элемента. Дальше указан список переменных среды, который команда strace опустила для краткости.
Следующие примерно 30 строк отражают работу механизма загрузки стандартной библиотеки языка С из библиотечного файла. Ближе к концу наконец встречаются системные вызовы, связанные непосредственно с работой программы. Системный вызов uname() запрашивает имя компьютера у ядра:
Uname({sys="Linux", node="myhostname", ...}) = 0.
Заметьте, что команда strace показала метки полей структуры, в которой хранятся аргументы. Эта структура заполняется в системном вызове: Linux помещает в поле sys имя операционной системы, а в поле node — имя компьютера. Функция uname() будет описана ниже, в разделе 8.15. "Функция uname()".
Системный вызов write() выводит полученные результаты на экран. Вспомните, что дескриптор 1 соответствует стандартному выходному потоку. Третий аргумент — это количество отображаемых символов. Функция возвращает число действительно записанных символов.
Write(1, "myhostname\n", 11) = 11.
Эта строка может отобразиться искаженной, поскольку вывод программы hostname смешивается с результатами работы команды strace. Если запускаемая программа создает слишком много выходных данных, лучше перенаправить вывод команды strace в файл с помощью опции -о имя_файла.
8.2. Функция access(): проверка прав доступа к файлу.
Функция access() определяет, имеет ли вызывающий ее процесс право доступа к заданному файлу. Функция способна проверить любую комбинацию привилегий чтения, записи и выполнения, а также факт существования файла.
Функция access() принимает два аргумента: путь к проверяемому файлу и битовое объединение флагов R_OK, W_OK и X_OK, соответствующих правам чтения, записи и выполнения. При наличии у процесса всех необходимых привилегий функция возвращает 0. Если файл существует, а нужные привилегии на доступ к нему у процесса отсутствуют, возвращается -1 и в переменную errno записывается код ошибки EACCES (или EROFS, если проверяется право записи в файл, который расположен в файловой системе, смонтированной только для чтения).
Если второй аргумент равен F_OK, функция access() проверяет лишь факт существования файла. В случае обнаружения файла возвращается 0, иначе — -1 (в переменную errno помещается также код ошибки ENOENT). Когда один из каталогов на пути к файлу недоступен, в переменную errno будет помещён код EACCES.
Программа, показанная в листинге 8.1, с помощью функции access() проверяет существование файла и определяет, разрешен ли к нему доступ на чтение/запись. Имя файла задается в командной строке.
Листинг 8.1. (check-access.c) Проверка прав доступа к файлу.
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
Int main(int argc, char* argv[]) {
char* path = argv[1];
int rval;
/* Проверка существования файла. */
rval = access(path, F_OK);
if (rval == 0).
printf("%s exists\n", path);
else {
if (errno == ENOENT).
printf("%s does not exist\n", path);
else if (errno == EACCES).
printf("%s is not accessible\n", path);
return 0;
}
/* Проверка права доступа. */
rval = access(path, R_OK);
if (rval == 0).
printf("%s is readable\n", path);
else.
printf("%s is not readable (access denied)\n", path);
/* проверка права записи. */
rval = access(path, W_OK);
if (rval == 0).
printf("%s is writable\n", path);
else if (errno == EACCES).
printf("%s is not writable (access denied)\n", path);
else if (errno == EROFS).
printf("%s is not writable (read-only filesystem)\n",
path);
return 0;
}
Вот как, к примеру, проверить права доступа к файлу README, расположенному на компакт-диске:
% ./check-access /mnt/cdrom/README.
/mnt/cdrom/README exists.
/mnt/cdrom/README is readable.
/mnt/cdrom/README is not writable (read-only filesystem).
8.3. Функция fcntl(): блокировки и другие операции над файлами.
Функция fcntl() — это точка доступа к нескольким особым операциям над файлами. Первым аргументом функции является дескриптор файла, вторым указывается код операции. Для некоторых операций требуется также дополнительный, третий аргумент. В этом разделе описана наиболее распространенная операция, выполняемая с помощью функции fcntl(): блокирование файлов.
Функция fcntl() позволяет программе поставить на файл блокировку чтения иди записи. Это напоминает применение исключающих семафоров, которые описывались в главе 5, "Взаимодействие процессов". Блокировка чтения ставится на файл, доступный для чтения. Соответственно блокировка записи ставится на файл, доступный для записи. Несколько процессов могут удерживать блокировку чтения одного и того же файла, но только одному процессу разрешено ставить блокировку записи. Файл не может быть одновременно заблокирован и для чтения, и для записи. Учтите, что наличие блокировки не мешает другим процессам открывать файл и осуществлять чтение/запись его данных, если только они сами не попытаются вызвать функцию fcntl().
Прежде чем ставить блокировку на файл, необходимо создать и обнулить структуру типа flock. В поле l_type должна быть записана константа F_RDLCK в случае блокировки чтения и константа F_WRLCK — в случае блокировки записи. Далее следует вызвать функцию fcntl(), передав ей дескриптор файла, код операции F_SETLCKW и указатель на структуру типа flock. Если аналогичная блокировка уже была поставлена другим процессом, функция fcntl() перейдет в режим ожидания, пока "мешающая" ей блокировка не будет снята.
В листинге 8.2 показана программа, которая открывает для записи указанный файл, а затем ставит на него блокировку записи. Программа ждет нажатия клавиши <Enter>, после чего снимает блокировку и закрывает файл.
Листинг 8.2. (lock-file.c) Установка блокировки записи с помощью функции fcntl().
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
Int main(int argc, char* argv[]) {
char* file = argv[1];
int fd;
struct flock lock;
printf("opening %s\n", file);
/* Открытие файла. */
fd = open(file, O_WRONLY);
printf("locking\n");
/* инициализация структуры flock. */
memset(&lock, 0, sizeof(lock));
lock.l_type = F_WRLCK;
/* Установка блокировки записи. */
fcntl(fd, F_SETLKW, &lock);
printf("locked; hit Enter to unlock... ");
/* Ожидание нажатия клавиши <Enter>. */
getchar();
printf("unlocking\n");
/* Снятие блокировки. */
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &lock);
close(fd);
return 0;
}
Скомпилируйте программу и запустите ее с каким-нибудь тестовым файлом, скажем, /tmp/test-file:
% cc -o lock-file lock-file.с.
% touch /tmp/test-file.
% ./lock-file /tmp/test-file.
Opening /tmp/test-file.
Locking.
Locked; hit Enter to unlock...
Теперь откройте другое окно и вызовите программу еще раз с тем же файлом:
% ./lock-file /tmp/test-file.
Opening /tmp/test-file.
Locking.
Пытаясь поставить блокировку на файл, программа сама окажется заблокированной. Вернитесь в первое окно и нажмите <Enter>:
Unlocking.
В результате программа, запущенная во втором окне, немедленно продолжит свою работу. Если необходимо, чтобы функция fcntl() не переходила в режим ожидания в случае, когда блокировку поставить невозможно, задайте в качестве кода операции константу F_SETLCK, а не F_SETLKW. Если функция обнаружит, что запрашиваемый файл уже заблокирован, она немедленно вернет -1.
В Linux имеется системный вызов flock(), также реализующий операцию блокирования файла. Но у функции fcntl() есть большое преимущество: она работает с файловыми системами NFS[28] (при условии, что сервер NFS имеет относительно недавнюю версию и сконфигурирован правильно). Так что. имея доступ к двум компьютерам, которые монтируют одну и ту же файловую систему через NFS, можно повторить показанный выше пример на двух разных машинах.
8.4. Функции fsync() и fdatasync(): очистка дисковых буферов.
В большинстве операционных систем при записи в файл данные не передаются на диск немедленно. Вместо этого операционная система помещает их в резидентный кэш-буфер с целью сокращения числа обращений к диску и повышения оперативности программы. Когда буфер заполнится или произойдет какое-нибудь другое событие (например, истечет определенный промежуток времени), система запишет содержимое буфера на диск в ходе одной непрерывной операции.
В Linux тоже поддерживается такой тип кэширования. Обычно он способствует существенному повышению производительности. Но он же делает ненадежными программы, зависящие от целостности дисковых данных. Если система внезапно выйдет из строя, например вследствие сбоя ядра или отключения питания, любые данные, находящиеся в памяти и еще не записанные на диск, будут потеряны.
Предположим, создается программа обработки транзакций, которая ведет журнальный файл. В этот файл помещаются записи обо всех транзакциях, завершившихся на данный момент, чтобы в случае системного сбоя можно было восстановить целостность данных. Очевидно, не менее важна и целостность самого журнального файла: как только транзакция завершена, запись о ней должна быть немедленно занесена в дисковый файл.
Для реализации такого поведения ОС Linux предоставляет системный вызов fsync(). Эта функция принимает один аргумент — дескриптор записываемого файла — и принудительно переносит на диск все данные этого файла, находящиеся в кэш-буфере. Функция не завершается до тех пор, пока данные не окажутся на диске.
В листинге 8.3 показана функция, использующая данный системный вызов. Она записывает переданную ей строку в журнальный файл.
Листинг 8.3. (write_journal_entry.c) Запись строки в журнальный файл с последующей синхронизацией.
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
Const char* journal_filename = "journal.log";
Void write_journal_entry(char* entry) {
int fd =
open(journal_filename,
O_WRONLY | O_CREAT | O_APPEND, 0660);
write(fd, entry, strlen(entry));
write(fd, "\n", 1);
fsync(fd);
close(fd);
}
Аналогичное действие выполняет другой системный вызов: fdatasync(). Но если функция fsync() гарантирует, что дата модификации файла будет обновлена, то функция fdatasync() этого не делает, а лишь гарантирует запись данных. В принципе это означает, что функция fdatasync() способна выполняться быстрее, чем fsync(), так как ей требуется выполнить одну операцию записи на диск, а не две. Но в настоящее время в Linux обе функции работают одинаково, обновляя дату модификации.
Файл можно также открыть в режиме синхронного ввода-вывода, при котором все операции записи будут немедленно фиксироваться на диске. Для этого в функции open() следует указать флаг O_SYNC.
8.5. Функции getrlimit() и setrlimit(): лимиты ресурсов.
Функции getrlimit() и setrlimit() позволяют процессу определять и задавать лимиты использования системных ресурсов. Аналогичные действия выполняет команда ulimit, которая ограничивает доступ запускаемых пользователем программ к ресурсам.
У каждого ресурса есть два лимита: жесткий и нежесткий. Второе значение никогда не может быть больше первого, и лишь процессы с привилегиями супер пользователя имеют право менять жесткий лимит. Обычно приложение уменьшает нежесткий лимит, ограничивая потребление системных ресурсов.
Обе функции принимают два аргумента: код, задающий тип ограничения, и указатель на структуру типа rlimit. Функция getrlimit() заполняет поля этой структуры, тогда как функция setrlimit() проверяет их и соответствующим образом меняет лимит. У структуры rlimit два поля: в поле rlim_cur содержится значение нежесткого лимита, а в поле rlim_max — значение жесткого лимита.
Ниже перечислены коды наиболее полезных лимитов, допускающих возможность изменения.
■ RLIMIT_CPU. Это максимальный интервал времени центрального процессора (в секундах), занимаемый программой. Именно столько времени отводится программе на доступ к процессору. В случае превышения данного ограничения программа будет завершена по сигналу SIGXCPU.
■ RLIMIT_DATA. Это максимальный объем памяти, который программа может запросить для своих данных. Запросы на дополнительную память будут отвергнуты системой.
■ RLIMIT_NPROC. Это максимальное число дочерних процессов, которые могут быть запущены пользователем. Если процесс вызывает функцию fork(), а лимит уже исчерпал, функция завершается ошибкой.
■ RLIMIT_NOFILE. Это максимальное число файлов, которые могут быть одновременно открыты процессом.
Программа, приведенная в листинге 8.4, задает односекундный лимит использования центрального процессора, после чего переходит в бесконечный цикл. Как только программа превышает установленный ею же лимит, ОС Linux уничтожает ее.
Листинг 8.4. (limit-cpu.c) Задание ограничения на использование нейтрального процессора.
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>
Int main() {
struct rlimit rl;
/* Определяем текущие лимиты. */
getrlimit(RLIMIT_CPU, &rl);
/* Ограничиваем время доступа к процессору.
одной секундой. */
rl.rlim_cur = 1;
setrlimit(RLIMIT_CPU, &rl);
/* Переходим в бесконечный цикл. */
while(1);
return 0;
}
Когда программа завершается по сигналу SIGXCPU, интерпретатор команд выдает поясняющее сообщение:
% ./limit_cpu.
CPU time limit exceeded.
8.6. Функция getrusage(): статистика процессов.
Функция getrusage() запрашивает у ядра статистику работы процессов. Если первый аргумент функции равен RUSAGE_SELF, процесс получит информацию о самом себе. Если же первым аргументом является константа RUSAGE_CHILDREN, будет выдана информация обо всех его завершившихся дочерних процессах. Второй аргумент — это указатель на структуру типа rusage, в которую заносятся статистические данные.
Перечислим наиболее интересные поля этой структуры.
■ ru_utime. Здесь находится структура типа timeval, в которой указано, сколько пользовательского времени (в секундах) ушло на выполнение процесса. Это время, затраченное центральным процессором на выполнение программного кода, а не системных вызовов.
■ ru_stime. Здесь находится структура типа timeval, в которой указано, сколько системного времени (в секундах) ушло на выполнение процесса. Это время, затраченное центральным процессором на выполнение системных вызовов от имени данного процесса.
■ ru_maxrss. Это максимальный объем физической памяти, которую процесс занимал в какой-то момент своего выполнения.
В листинге 8.5 приведена функция, которая показывает, сколько пользовательского и системного времени потребил текущий процесс.
Листинг 8.5. (prinf-cpu-times.c) Определение пользовательского и системного времени, затраченного на выполнение текущего процесса.
#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>
Void print_cpu_time() {
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
printf("CPU time: %ld.%061d sec user, %ld.%061d sec system\n",
usage.ru_utime.tv_sec, usage.ru_utime.tv_usec,
usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
}
8.7, Функция gettimeofday(): системные часы.
Функция gettimeofday() определяет текущее системное время. В качестве аргумента она принимает структуру типа timeval, в которую записывается значение времени (в секундах), прошедшее с начала эпохи UNIX (1-е января 1970 г., полночь по Гринвичу). Это значение разделяется на два поля. В поле tv_sec хранится целое число секунд, а в поле tv_usec — дополнительное число микросекунд. У функции есть также второй аргумент, который должен быть равен NULL. Функция объявлена в файле <sys/time.h>.
Результат, возвращаемый функцией gettimeofday(), мало подходит для отображения на экране, поэтому существуют библиотечные функции localtime() и strftime(), преобразующие это значение в нужный формат. Функция localtime() принимает указатель на число секунд (поле tv_sec структуры timeval) и возвращает указатель на структуру типа tm. Эта структура содержит поля, заполняемые параметрами времени в соответствии с локальным часовым поясом:
■ tm_hour, tm_min, tm_sec — текущее время (часы, минуты, секунды);
■ tm_year, tm_mon, tm_day — год, месяц, день;
■ tm_wday — день недели (значение 0 соответствует воскресенью);
■ tm_yday — день года;
■ tm_isdst — флаг, указывающий, учтено ли летнее время.
Функция strftime() на основании структуры tm создает строку, отформатированную по заданному правилу. Формат напоминает тот, что используется в функции printf(): указывается строка с кодами, определяющими включаемые поля структуры. Например, форматная строка вида.
"%Y-%m-%d %Н:%М:%S".
Соответствует такому результату:
2001-01-14 13:09:42.
Функции strftime() необходимо задать указатель на текстовый буфер, куда будет помещена полученная строка, длину буфера, строку формата и указатель на структуру типа tm. Следует учесть, что ни функция localtime(), ни функция strftime() не учитывают дробную часть текущего времени (поле tv_usec структуры timeval). Об этом должен позаботиться программист.
Объявления функций localtime() и strftime() находятся в файле <time.h>.
Программа, показанная в листинге 8.6, отображает текущие дату и время с точностью до миллисекунды.
Листинг 8.6. (print-time.c) Отображение даты и времени.
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
Void print_time() {
struct timeval tv;
struct tm* ptm;
char time_string[40];
long milliseconds;
/* Определение текущего времени и преобразование полученного.
значения в структуру типа tm. */
gettimeofday(&tv, NULL);
ptm = localtime(&tv.tv_sec);
/* Форматирование значения даты и времени с точностью.
до секунды. */
strftime(time_string, sizeof(time_string),
"%Y-%m-%d %H:%M:%S", ptm);
/* Вычисление количества миллисекунд. */
milliseconds = tv.tv_usec / 1000;
/* Отображение даты и времени с указанием.
числа миллисекунд. */
printf("%s.%03ld\n", time_string, milliseconds);
}
8.8. Семейство функций mlock(): блокирование физической памяти.
Функции семейства mlock() позволяют программе блокировать часть своего адресного пространства в физической памяти. Заблокированные страницы не будут выгружены операционной системой в раздел подкачки, даже если программа долго к ним не обращалась.
Блокирование физической памяти важно в программах реального времени, поскольку задержки, связанные с выгрузкой и подкачкой страниц, могут оказаться слишком длинными или возникать в самый неподходящий момент. Приложения, заботящиеся о безопасности своих данных, могут устанавливать запрет на выгрузку важных данных в файл подкачки, в котором они станут доступны злоумышленнику после завершения программы.
Чтобы заблокировать область памяти, достаточно вызвать функцию mlock(), передав ей указатель на начало области и значение длины области. ОС Linux разбивает память на страницы и соответственно блокирует ее постранично: любая страница, которую захватывает (хотя бы частично) заданная в функции mlock() область памяти, окажется заблокированной. Определить размер системной страницы позволяет функция getpagesize(). В Linux-системах, работающих на платформе x86, эта величина составляет 4 Кбайт.
Вот как можно выделить и заблокировать 32 Мбайт оперативной памяти:
Const int alloc_size = 32 * 1024 * 1024;
Char* memory = malloc(alloc_size);
Mlock(memory, alloc_size);
Выделение страницы и блокирование ее с помощью функции mlock() еще не означает, что эта страница будет предоставлена данному процессу, поскольку выделение памяти может происходить в режиме копирования при записи.[29] Следовательно, каждую страницу необходимо проинициализировать:
Size_t i;
Size_t page_size = getpagesize();
For (i = 0; i < alloc_size; i += page_size).
memory[i] = 0;
Процессу, осуществляющему запись на страницу, операционная система предоставит в монопольное использование ее уникальную копию.
Для разблокирования области памяти следует вызвать функцию munlock(), передав ей те же аргументы, что и функции mlock().
Функция mlockall() блокирует все адресное пространство программы и принимает единственный флаговый аргумент. Флаг MCL_CURRENT означает блокирование всей выделенной на данный момент памяти, но не той, что будет выделяться позднее. Флаг MCL_FUTURE задает блокирование всех страниц, выделенных после вызова функции mlockall(). Сочетание флагов MCL_CURRENT | MCL_FUTURE позволяет блокировать всю память программы, как текущую, так и будущую.
Блокирование больших объемов памяти, особенно с помощью функции mlockall(), несет потенциальную угрозу всей системе. Несправедливое распределение оперативной памяти приведет к катастрофическому снижению производительности системы, так как остальным процессам придется сражаться друг с другом за небольшой "клочок" памяти, вследствие чего они будут постоянно выгружаться на диск и загружаться обратно. Может даже возникнуть ситуация, когда оперативная память закончится и система начнет уничтожать процессы. По этой причине функции mlock() и mlockall() доступны лишь суперпользователю. Если какой-нибудь другой пользователь попытается вызвать одну из этих функций, она вернёт значение -1, а в переменную errno будет записан код EPERM.
Функция munlоскаll() разблокирует всю память текущего процесса.
Контролировать использование памяти удобнее всего с помощью команды top. В колонке SIZE ее выходных данных показывается размер виртуального адресного пространства каждой программы (общий размер сегментов кода, данных и стека с учетом выгруженных страниц). В колонке RSS приводится объем резидентной части программы. Сумма значений в столбце RSS не может превышать имеющийся объем ОЗУ, а суммарный показатель по столбцу SIZE не может быть больше 2 Гбайт (в 32-разрядных версиях Linux).
Функции семейства mlock() объявлены в файле <sys/mman.h>.
8.9. Функция mprotect(): задание прав доступа к памяти.
В разделе 5.3, "Отображение файлов в памяти", рассказывалось о том, как осуществляется отображение файла в памяти. Вспомните, что третьим аргументом функции mmap() является битовое объединение флагов доступа: флаги PROT_READ, PROT_WRITE и PROT_EXEC задают права чтения, записи и выполнения файла, а флаг PROT_NONE означает запрет доступа. Если программа пытается выполнить над отображаемым файлом недопустимую операцию, ей посылается сигнал SIGSEGV (нарушение сегментации), который приводит к завершению программы.
После того как файл был отображен в памяти, изменить права доступа к нему позволяет функция mprotect(). Ее аргументами является адрес области памяти, размер области и новый набор флагов доступа. Область должна состоять из целых страниц, т.е. начинаться и заканчиваться на границе между страницами.
Корректное выделение памяти. Учтите, что память, выделяемая функцией malloc(), обычно не выравнивается по границе страниц, даже если размер области кратен размеру страницы. Если требуется защищать память, выделяемую функцией malloc(), нужно запросить более крупный блок, а затем найти в нем участок, выровненный по границе страниц. Кроме того, с помощью функции mmap() можно обойти функцию malloc() и запрашивать память непосредственно у ядра Linux.Предположим, к примеру, что программа выделяет страницу, отображая в памяти файл /dev/zero. Память инициализируется как для чтения, так и для записи:
Int fd = open("/dev/zero", O_RDONLY);
Char* memory =
mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0);
Close(fd);
Далее программа запрещает запись в эту область памяти, вызывая функцию mprotect():
Mprotect(memory, page_size, PROT_READ);
Существует оригинальная методика контроля памяти: можно защитить область памяти с помощью функций mmap() и mprotect(), а затем обрабатывать сигнал SIGSEGV, посылаемый при попытке доступа к этой памяти. Эта методика иллюстрируется в листинге 8.7.
Листинг 8.7. (mprotect.c) Обнаружение попыток доступа к памяти благодаря функции mprotect().
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
Static int alloc_size;
Static char* memory;
Void segv_handler(int signal_number) {
printf("memory accessed!\n");
mprotect(memory, alloc_size, PROT_READ | PROT_WRITE);
}
Int main() {
int fd;
struct sigaction sa;
/* Назначение функции segv_handler() обработчиком сигнала.
SIGSEGV. */
memset(&sa, 0, sizeof(sa));
sa.sa_handler = &segv_handler;
sigaction(SIGSEGV, &sa, NULL);
/* Выделение одной страницы путем отображения в памяти файла.
/dev/zero. Сначала память доступна только для записи. */
alloc_size = getpagesize();
fd = open("/dev/zero", O_RDONLY);
memory =
mmap(NULL, alloc_size, PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
/* Запись на страницу для получения ее копии в частное.
использование. */
memory[0] = 0;
/* Запрет на запись в память. */
mprotect(memory, alloc_size, PROT_NONE);
/* Попытка записи в память. */
memory[0] = 1;
/* Удаление памяти. */
printf("all done\n");
munmap(memory, alloc_size);
return 0;
}
Программа работает по следующей схеме.
1. Задается обработчик сигнала SIGSEGV.
2. Файл /dev/zero отображается в памяти, из которой выделяется одна страница. В эту страницу записывается инициализирующее значение, благодаря чему программе предоставляется частная копия страницы.
3. Программа защищает память, вызывая функцию mprotect() с флагом PROT_NONE.
4. Когда программа впоследствии обращается к памяти, Linux посылает ей сигнал SIGSEGV, который обрабатывается в функции segv_handler(). Обработчик сигнала отменяет защиту памяти, разрешая выполнить операцию записи.
5. Программа удаляет область память с помощью функции munmap().
8.10. Функция nanosleep(): высокоточная пауза.
Функция nanosleep() является более точной версией стандартной функции sleep(), принимая указатель на структуру типа timespec, где время задается с точностью до наносекунды, а не секунды. Правда, особенности работы ОС Linux таковы, что реальная точность оказывается равной 10 мс, но это все равно выше, чем в функции sleep(). Функцию nanosleep() можно использовать в приложениях, где требуется запускать различные операции с короткими интервалами между ними.
В структуре timespec имеются два поля:
■ tv_sес — целое число секунд;
■ tv_nsec — дополнительное число миллисекунд (должно быть меньше, чем 109).
Работа функции nanosleep(), как и функции sleep(), прерывается при получении сигнала. При этом функция возвращает значение -1, а в переменную errno записывается код EINTR. Но у функции nanosleep() есть важное преимущество. Она принимает дополнительный аргумент — еще один указатель на структуру timespec, в которую (если указатель не равен NULL) заносится величина оставшегося интервала времени (т.е. разница между запрашиваемым и прошедшим промежутками времени). Благодаря этому можно легко возобновлять прерванные операции ожидания.
В листинге 8.8 показана альтернативная реализация функции sleep(). В отличие от стандартного системного вызова эта функция может принимать дробное число секунд и возобновлять операцию ожидания в случае прерывания по сигналу.
Листинг 8.8. (better_sleep.c) Высокоточная реализация функции sleep().
#include <errno.h>
#include <time.h>
Int better_sleep(double sleep_time) {
struct timespec tv;
/* Заполнение структуры timespec на основании указанного числа.
секунд. */
tv.tv_sec = (time_t)sleep_time;
/* добавление неучтенных выше наносекунд. */
tv.tv_nsec = (long)((sleep_time - tv.tv_sec) * 1e+9);
while (1) {
/* Пауза, длительность которой указана в переменной tv.
В случае прерывания по сигналу величина оставшегося.
промежутка времени заносится обратно в переменную tv. */
int rval = nanosleep(&tv, &tv);
if (rval == 0).
/* пауза успешно окончена. */
return 0;
else if (errno == EINTR).
/* Прерывание по сигналу. Повторная попытка. */
continue;
else.
/* Какая-то другая ошибка. */
return rval;
}
return 0;
}
8.11. Функция readlink(): чтение символических ссылок.
Функция readlink() определяет адресата символической ссылки. Она принимает три аргумента: путь к символической ссылке, буфер для записи адресата и длина буфера. Как ни странно, путевое имя, помещаемое в буфер, не завершается нулевым символом. Но поскольку в третьем аргументе возвращается длина буфера, добавить этот символ несложно.
Если первый аргумент не является символической ссылкой, функция readlink() возвращает -1, а в переменную errno записывается константа EINVAL.
Программа, представленная в листинге 8.9, показывает адресата символической ссылки, заданной в командной строке.
Листинг 8.9. (print-symlink.с) Отображение адресата символической ссылки.
#include «errno.h>
#include <stdio.h>
#include <unistd.h>
Int main(int argc, char* argv[]) {
char target_path[256];
char* link_path = argv[1];
/* Попытка чтения адресата символической ссылки. */
int len =
readlink(link_path, target_path, sizeof(target_path));
if (len == -1) {
/* Функция завершилась ошибкой. */
if (errno == EINVAL).
/* Это не символическая ссылка. */
fprintf(stderr, "%s is not a symbolic link\n", link_path);
else.
/* Произошла какая-то другая ошибка. */
perror("readlink");
return 1;
} else {
/* Завершаем путевое имя нулевым символом. */
target_path[len] = '\0';
/* Выводим результат. */
printf("%s\n", target_path);
return 0;
}
}
Ниже показано, как создать символическую ссылку и проверить ее с помощью программы print-symlink:
% ln -s /usr/bin/wc my_link.
% ./print-symlink my_link.
/usr/bin/wc.
8.12. Функция sendfile(): быстрая передача данных.
Функция sendfile() — это эффективный механизм копирования данных из одного файлового дескриптора в другой. Дескрипторам могут соответствовать дисковые файлы, сокеты или устройства.
Обычно цикл копирования реализуется следующим образом. Программа выделяет буфер фиксированного размера, перемещает в него данные из исходного дескриптора, затем записывает содержимое буфера во второй дескриптор и повторяет описанную процедуру до тех пор, пока не будут скопированы все данные. Такая схема неэффективна как с точки зрения времени, так и с точки зрения затрат памяти, поскольку выделяется дополнительный буфер и над его содержимым выполняются операции копирования.
Функция sendfile() устраняет потребность в создании промежуточного буфера. Ей передаются дескриптор для записи, дескриптор для чтения, указатель на переменную смещения и число копируемых данных. Переменная смещения определяет позицию входного файла, с которой начинается копирование (0 — это начало файла). После окончания копирования переменная будет содержать смещение конца блока. Функция sendfile() объявлена в файле <sys/sendfile.h>.
Программа, показанная в листинге 8.10, представляет собой простую, но очень эффективную реализацию механизма файлового копирования. Она принимает в командной строке два имени файла и копирует содержимое первого файла во второй. Размер исходного файла определяется с помощью функции fstat().
Листинг 8.10. (сору.с) Копирование файла с помощью функции sendfile().
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
Int main(int argc, char* argv[]) {
int read_fd;
int write_fd;
struct stat stat_buf;
off_t offset = 0;
/* Открытие входного файла. */
read_fd = open(argv[1], O_RDONLY);
/* Определение размера входного файла. */
fstat(read_fd, &stat_buf);
/* Открытие выходного файла для записи. */
write_fd =
open(argv[2], O_WRONLY | O_CREAT, stat_buf.st_mode);
/* Передача данных из одного файла в другой. */
sendfile(write_fd, read_fd, &offset, stat_buf.st_size);
/* Закрытие файлов. */
close(read_fd);
close(write_fd);
return 0;
}
Функция sendfile() часто используется для повышения эффективности копирования. Она широко применяется Web-серверами и сетевыми демонами, предоставляющими файлы по сети клиентским программам. Запрос обычно поступает через сокет. Серверная программа открывает локальный дисковый файл, извлекает из него данные и записывает их в сокет. Благодаря функции sendfile() эта операция существенно ускоряется.
8.13. Функция setitimer(): задание интервальных таймеров.
Функция setitimer() является обобщением системного вызова alarm(). Она планирует доставку сигнала по истечении заданного промежутка времени.
С помощью функции setitimer() можно создавать таймеры трех типов.
■ ITIMER_REAL. По истечении указанного времени процессу посылается сигнал SIGALRM.
■ ITIMER_VIRTUAL. После того как процесс отработал требуемое время, ему посылается сигнал SIGVTALRM. Время, когда процесс не выполнялся (работало ядро или другой процесс), не учитывается.
■ ITIMER_PROF. По истечении указанного времени процессу посылается сигнал SIGPROF. Учитывается время выполнения самого процесса, а также запускаемых в нем системных вызовов.
Код таймера задается в первом аргументе функции setitimer(). Второй аргумент — это указатель на структуру типа itimerval, содержащую параметры таймера. Третий аргумент либо равен NULL, либо является указателем на другую структуру itimerval, куда будут записаны прежние параметры таймера.
В структуре itimerval два поля.
■ it_value. Здесь находится структура типа timeval, где записано время отправки сигнала. Если это поле равно нулю, таймер отменяется.
■ it_interval. Это еще одна структура timeval, определяющая, что произойдет после отправки первого сигнала. Если она равна нулю, таймер будет отменен. В противном случае здесь записан интервал генерирования сигналов.
Структура timeval была описана в разделе 8.7. "Функция gettimeofday(): системные часы".
В листинге 8.11 показано, как с помощью функции setitimer() отслеживать выполнение программы. Таймер настроен на интервал 250 мс, по истечении которого генерируется сигнал SIGVTALRM.
Листинг 8.11. (itimer.c) Пример создания таймера.
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
Void timer_handler(int signum) {
static int count = 0;
printf("timer expired %d times\n", ++count);
}
Int main() {
struct sigaction sa;
struct itimerval timer;
/* Назначение функции timer_handler обработчиком сигнала.
SIGVTALRM. */
memset(&sa, 0, sizeof(sa));
sa.sa_handler = &timer_handler;
sigaction(SIGVTALRM, &sa, NULL);
/* Таймер сработает через 250 миллисекунд... */
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = 250000;
/* ... и будет продолжать активизироваться каждые 250.
миллисекунд. */
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = 250000;
/* Запуск виртуального таймера. Он подсчитывает фактическое.
время работы процесса. */
setitimer(ITIMER_VIRTUAL, &timer, NULL);
/* Переход в бесконечный цикл. */
while (1);
}
8.14. Функция sysinfo(): получение системной статистики.
Функция sysinfo() возвращает системную статистике. Ее единственным аргументом является указатель на структуру типа sysinfo. Перечислим наиболее интересные поля этой структуры.
■ uptime — время в секундах, прошедшее с момента загрузки системы;
■ totalram — общий объем оперативной памяти;
■ freeram — свободный объем ОЗУ;
■ procs — число процессов, работающих в системе.
Для использования функции sysinfo() требуется включить в программу файлы <linux/kernel.h>, <linux/sys.h> и <sys/sysinfo.h>.
Программа, приведенная в листинге 8.12, отображает статистическую информацию о текущем состоянии системы.
Листинг 8.12. (sysinfo.c) Вывод системной статистики.
#include <linux/kernel.h>
#include <linux/sys.h>
#include <stdio.h>
#include <sys/sysinfo.h>
Int main() {
/* Константы преобразования. */
const long minute = 60;
const long hour = minute * 60;
const long day = hour * 24;
const double megabyte = 1024 * 1024;
/* Получение системной статистики. */
struct sysinfo si;
sysinfo(&si);
/* Представление информации в понятном виде. */
printf("system uptime : %ld days, %ld:%02ld:%021d\n",
si.uptime / day, (si.uptime % day) / hour,
(si.uptime % hour) / minute, si.uptime % minute);
printf("total RAM : %5.1f MB\n", si.totalram / megabyte);
printf("free RAM : %5.1f MB\n",
si.freeram / megabyte);
printf("process count : %d\n", si.procs);
return 0;
}
8.15. Функция uname().
Функция uname() возвращает информацию о системе, в частности сетевое и доменное имена компьютера, а также версию операционной системы. Единственным аргументом функции является указатель на структуру типа utsname. Функция заполняет следующие поля этой структуры (все эти поля содержат текстовые строки).
■ sysname. Здесь содержится имя операционной системы (например, Linux).
■ release, version. В этих полях указываются номера версии и модификации ядра.
■ machine. Здесь приводится информация о платформе, на которой работает система. В случае Intel-совместимых компьютеров это будет либо i386, либо i686, в зависимости от процессора.
■ node. Это имя компьютера.
■ __domain. Это имя домена.
Функция uname() объявлена в файле <sys/utsname.h>.
В листинге 8.13 показана небольшая программа, которая отображает номера версии и модификации ядра Linux, а также сообщает тип платформы.
Листинг 8.15. (print-uname.c) Вывод информации о ядре и платформе.
#include <stdio.h>
#include <sys/utsname.h>
Int main() {
struct utsname u;
uname(&u);
printf("%s release %s (version %s) on %s\n", u.sysname,
u.release, u.version, u.machine);
return 0;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.