Программирование для Linux. Профессиональный подход - Марк Митчелл
Шрифт:
Интервал:
Закладка:
В следующем фрагменте программы делается попытка открыть файл. Если это не получается, выводится сообщение об ошибке и программа завершает свою работу. Обратите внимание на то, что в случае успеха операции функция open() возвращает дескриптор открытого файла, иначе — -1.
fd = open("inputfile.txt", O_RDONLY);
if (fd == -1) {
/* Открыть файл не удалось.
Вывод сообщения об ошибке и выход. */
fprintf(stderr, "error opening file: %sn", strerror(errno));
exit(1);
}
В зависимости от особенностей программы и используемого системного вызова конкретные действия, предпринимаемые в случае ошибки, могут быть разными: вывод сообщения об ошибке, отмена операции, аварийное завершение программы, повторная попытка и даже игнорирование ошибки. Тем не менее важно включить в программу код, обрабатывающий все возможные варианты ошибок.
Одни из кодов, с которым приходится сталкиваться наиболее часто, особенно в функциях ввода-вывода, — это EINTR. Ряд функций, в частности read(), select() и sleep(), требует определенного времени на выполнение. Они называются блокирующими, так как выполнение программы приостанавливается до тех пор, пока функция не завершится. Но если программа, будучи заблокированной, принимает сигнал, функция завершается, не закончив выполнение операции. В данном случае в переменную errno записывается значение EINTR. Обычно в подобной ситуации следует повторно выполнить системный вызов.
Ниже приведен фрагмент программы, в котором функция chown() меняет владельца файла, определяемого переменной path, назначая вместо него пользователя с идентификатором user_id. Если функция завершается неуспешно, дальнейшие действия программы зависят от значения переменной errno. Обратите внимание на интересный момент: при обнаружении возможной ошибки в самой программе ее выполнение завершается с помощью функции abort() или assert(), вследствие чего генерируется файл дампа оперативной памяти. Анализ этого файла может помочь выяснить природу таких ошибок. В случае невосстанавливаемых ошибок, например нехватки памяти, программа завершается с помощью функции exit(), указывая ненулевой код ошибки: в подобных ситуациях файл дампа оказывается бесполезным.
rval = chown(path, user_id, -1);
if (rval != 0) {
/* Сохраняем переменную errno, поскольку она будет изменена
при следующем системном вызове. */
int error_code = errno;
/* Операция прошла неуспешно; в случае ошибки функция chown()
должна вернуть значение -1. */
assert(rval == -1);
/* Проверяем значение переменной errno и выполняем
соответствующее действие. */
switch (error_code) {
case EPERM: /* Доступ запрещен. */
case EROFS: /* Переменная PATH ссылается на файловую
систему, доступную только для чтения. */
case ENAMETOOLONG: /* Переменная PATH оказалась слишком длинной. */
case ENOENT: /* Переменная PATH ссылается на
несуществующий файл. */
case ENOTDIR: /* Один из компонентов переменной PATH
не является каталогом. */
case EACCES: /* Один из компонентов переменной PATH
недоступен. */
/* Что-то неправильно с файлом, выводим сообщение
об ошибке. */
fprintf(stderr, "error changing ownership of %s: %sn",
path, strerror(error_code));
/* He завершаем программу; можно предоставить пользователю
шанс открыть другой файл. */
break;
case ЕFAULT:
/* Переменная PATH содержит неправильный адрес. Это, скорее
всего, ошибка программы. */
abort();
case ENOMEM:
/* Ядро столкнулось с нехваткой памяти. */
fprintf(stderr, "%sn", strerror(error_code));
exit(1);
default:
/* Произошла какая-то другая, непредвиденная ошибка. Мы
пытались обработать все возможные коды ошибок. Если
что-то пропущено, то это ошибка программы! */
abort();
};
}
В самом начале программного фрагмента можно было поставить следующий код:
rval = chown(path, user_id, -1);
assert(rval == 0);
Но в таком случае, если функция завершится неуспешно, у нас не будет возможности обработать или исправить ошибку и даже просто сообщить о ней. Какую форму проверки использовать — зависит от требований к обнаружению и последующему исправлению ошибок в программе.
2.2.4. Ошибки выделения ресурсов
Обычно при неудачном выполнении системного вызова наиболее приемлемое решение — отменить текущую операцию, но не завершить программу, так как можно восстановить ее нормальную работу. Один из способов сделать это — выйти из текущей функции, передав через оператор return код ошибки вызывающему модулю.
В случае, когда выход осуществляется посреди функции, важно убедиться в том, что ресурсы, выделенные в функции ранее, освобождены. К таким ресурсам относятся буферы памяти, дескрипторы и указатели файлов, временные файлы, объекты синхронизации и т.д. В противном случае, если программа продолжит выполняться, ресурсы окажутся потерянными.
В качестве примера рассмотрим функцию, загружающую содержимое файла в буфер. Функция выполняет такую последовательность действий:
1. выделяет буфер;
2. открывает файл;
3. читает содержимое файла и записывает его в буфер;
4. закрывает файл;
5. возвращает буфер вызывающему модулю.
Если файл не существует, этап 2 закончится неудачей. Подходящая реакция в этом случае — вернуть из функции значение NULL. Но если буфер уже был выделен на этапе 1, существует опасность потери этого ресурса. Нужно не забыть освободить буфер где-то в программе. Если же неудачей завершится этап 3, требуется не только освободить буфер перед выходом из функции, но и закрыть файл.
В листинге 2.6 показан пример реализации такой функции.
Листинг 2.6. (readfile.c) Освобождение ресурсов при возникновении аварийных ситуаций#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
char* read_from_file(const char* filename, size_t length) {
char* buffer;
int fd;
ssize_t bytes_read;
/* Выделяем буфер. */
buffer = (char*)malloc(length);
if (buffer == NULL)
return NULL;
/* Открываем файл. */
fd = open(filename, O_RDONLY);
if (fd == 1) {
/* Открыть файл не удалось. Освобождаем буфер
перед выходом. */
free(buffer);
return NULL;
}
/* Чтение данных. */
bytes_read = read(fd, buffer, length);
if (bytes_read != length) {
/* Чтение не удалось. Освобождаем буфер и закрываем файл
перед выходом. */
free(buffer);
close(fd);
return NULL;
}
/* Все прошло успешно. Закрываем файл и возвращаем буфер
в программу. */
close(fd);
return buffer;
}
При завершении программы операционная система Linux освобождает выделенную память, ссылки на открытые файлы и большинство других ресурсов, поэтому перед вызовом функции exit() нет необходимости удалять буферы и закрывать файлы. Но некоторые другие совместно используемые ресурсы приходится все же освобождать вручную. В частности, это относится к временным файлам и совместным буферам памяти: они способны "пережить" программу.
2.3. Создание и использование библиотек
Практически со всеми программами компонуется одна или несколько библиотек. К любой программе, использующей функции языка С (например, printf() или malloc()), подключается библиотека времени выполнения. Если у программы есть графический интерфейс, вместе с ней компонуются библиотеки функций работы с окнами. Когда программа обращается к СУБД, она делает это посредством функции библиотеки, предоставленной разработчиком данной СУБД.
В каждом из перечисленных случаев необходимо решить, как компоновать библиотеку: статически или динамически. В первом случае программы станут громоздкими и их будет труднее обновлять, зато проще распространять. Во втором случае программы окажутся меньше и доступнее для изменений, но распространять придется большее число файлов. В данном разделе рассказывается о том, как осуществлять статическую и динамическую компоновку, на какие компромиссы при этом приходится идти и как решить, какой тип компоновки лучше всего подходит для конкретного случая.