Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 9 (всего у книги 55 страниц)
«Если неприятность может произойти, она случается»
– Закон Мерфи -
«Будь готов»
– Бойскауты -
Ошибки могут возникнуть в любое время. Диски могут заполниться, пользователи могут ввести неверные данные, сетевой сервер, с которого осуществляется чтение, может отказать, сеть может выйти из строя и т.д. Важно всегда проверять успешность завершения каждой операции.
Основные системные вызовы Linux почти всегда возвращают при ошибке -1 и 0 или положительное значение при успехе. Это дает возможность узнать, была операция успешной или нет:
int result;
result = some_system_call(param1, param2);
if (result < 0) {
/* ошибка, что-нибудь сделать */
} else
/* все нормально, продолжить */
Знания того, что произошла ошибка, недостаточно. Нужно знать, какая произошла ошибка. Для этого у каждого процесса есть предопределенная переменная с именем errno
. Всякий раз, когда системный вызов завершается ошибкой, errno
устанавливается в один из набора предопределенных значений ошибок errno
и предопределенные значения ошибок определены в файле заголовка
.
#include
extern int errno;
Хотя сама errno
может быть макросом, который действует подобно переменной int
– она не обязательно является действительной целой переменной. В частности, в многопоточном окружении у каждого потока будет своя индивидуальная версия errno
. Несмотря на это, практически для всех системных вызовов и функций в данной книге вы можете рассматривать errno
как простую int
.
errno
Стандарт POSIX 2001 определяет большое число возможных значений для errno. Многие из них относятся к сетям, IPC или другим специальным задачам. Справочная страница для каждого системного вызова описывает возможные значения errno
, которые могут иметь место; поэтому вы можете написать код для проверки отдельных ошибок и соответствующим образом обработать их, если это нужно. Возможные значения определены через символические имена. Предусмотренные GLIBC значения перечислены в табл. 4.1.
Таблица 4.1. Значения GLIBC для errno
E2BIG | Слишком длинный список аргументов |
EACCESS | Доступ запрещен |
EADDRINUSE | Адрес используется |
EADDRNOTAVAIL | Адрес недоступен |
EAFNOSUPPORT | Семейство адресов не поддерживается |
EAGAIN | Ресурс недоступен, попытайтесь снова (может быть то же самое значение, что EWOULDBLOCK ). |
EALREADY | Соединение уже устанавливается |
EBADF | Ошибочный дескриптор файла. |
EBADMSG | Ошибочное сообщение. |
EBUSY | Устройство или ресурс заняты |
ECANCELED | Отмена операции. |
ECHILD | Нет порожденного процесса. |
ECONNABORTED | Соединение прервано |
ECONNFRFUSED | Соединение отклонено |
ECONNRESET | Восстановлено исходное состояние соединения. |
EDEADLK | Возможен тупик (deadlock) в запросе ресурса. |
EDESTADDRREQ | Требуется адрес назначения |
EDOM | Математический аргумент выходит за область определения функции |
EDQUOT | Зарезервировано. |
EEXIST | Файл существует. |
EFAULT | Ошибочный адрес. |
EFBIG | Файл слишком большой. |
EHOSTUNREACH | Хост недоступен. |
EIDRM | Идентификатор удален |
EILSEQ | Ошибочная последовательность байтов. |
EINPROGRESS | Операция исполняется. |
EINTR | Прерванная функция. |
EINVAL | Недействительный аргумент. |
EIO | Ошибка ввода/вывода. |
EISCONN | Сокет (уже) соединен. |
EISDIR | Это каталог. |
ELOOP | Слишком много уровней символических ссылок. |
EMFILE | Слишком много открытых файлов. |
EMLINK | Слишком много ссылок. |
EMSGSIZE | Сообщение слишком длинное. |
EMULTIHOP | Зарезервировано. |
ENAMETOOLONG | Имя файла слишком длинное |
ENETDOWN | Сеть не работает |
ENETRESET | Соединение прервано сетью |
ENETUNREACH | Сеть недоступна. |
ENFILE | В системе открыто слишком много файлов. |
ENOBUFS | Буферное пространство недоступно. |
ENODEV | Устройство отсутствует |
ENOENT | Файл или каталог отсутствуют |
ENOEXEC | Ошибочный формат исполняемого файла. |
ENOLCK | Блокировка недоступна. |
ENOLINK | Зарезервировано. |
ENOMEM | Недостаточно памяти. |
ENOMSG | Сообщение нужного типа отсутствует |
ENOPROTOOPT | Протокол недоступен. |
ENOSPC | Недостаточно памяти в устройстве. |
ENOSYS | Функция не поддерживается. |
ENOTCONN | Сокет не соединен. |
ENOTDIR | Это не каталог |
ENOTEMPTY | Каталог не пустой. |
ENOTSOCK | Это не сокет |
ENOTSUP | Не поддерживается |
ENOTTY | Неподходящая операция управления вводом/выводом |
ENXIO | Нет такого устройства или адреса. |
EOPNOTSUPP | Операция сокета не поддерживается |
EOVERFLOW | Слишком большое значение для типа данных |
EPERM | Операция не разрешена |
EPIPE | Канал (pipe) разрушен |
EPROTO | Ошибка протокола. |
EPROTONOSUPPORT | Протокол не поддерживается |
EPROTOTYPE | Ошибочный тип протокола для сокета |
ERANGE | Результат слишком большой |
EROFS | Файловая система только для чтения |
ESPIPE | Недействительный поиск |
ESRCH | Нет такого процесса |
ESTALE | Зарезервировано |
ETIMEDOUT | Тайм-аут соединения истек |
ETXTBSY | Текстовый файл занят |
EWOULDBLOCK | Блокирующая операция (может быть то же значение, что и для EAGAIN ) |
EXDEV | Ссылка через устройство (cross-device link) |
Многие системы предоставляют также другие значения ошибок, а в более старых системах может не быть всех перечисленных значений ошибок. Полный список следует проверить с помощью справочных страниц intro(2) и errno(2) для локальной системы.
ЗАМЕЧАНИЕ.
errno
следует проверять лишь после того, как возникла ошибка и до того, как сделаны дальнейшие системные вызовы. Начальное значение той переменной 0. Однако, в промежутках между ошибками ничто не изменяет ее значения, это означает, что успешный системный вызов не восстанавливает значение 0. Конечно, вы можете вручную установить ее в 0 в самом начале или когда захотите, однако это делается редко.
Сначала мы используем errno
лишь для сообщений об ошибках. Для этого имеются две полезные функции. Первая – perror()
:
#include
void perror(const char *s);
Функция perror()
выводит предоставленную программой строку, за которой следует двоеточие, а затем строка, описывающая значение errno
:
if (some_system_call(param1, param2) < 0) {
perror("system call failed");
return 1;
}
Мы предпочитаем функцию strerror()
, которая принимает параметр со значением ошибки и возвращает указатель на строку с описанием ошибки:
#include
char *strerror(int errnum);
strerror()
предоставляет для сообщений об ошибках максимальную гибкость, поскольку fprintf()
дает возможность выводить ошибки любым нужным нам способом, наподобие этого.
if (some_system_call(param1, param2) < 0) {
fprintf(stderr, "%s: %d, %d: some_system_call failed: %sn",
argv[0], param1, param2, strerror(errno));
return 1;
}
По всей книге вы увидите множество примеров использования обеих функций.
Для использования в сообщениях об ошибках С предоставляет несколько специальных макросов. Наиболее широкоупотребительными являются __FILE__
и __LINE__
, которые разворачиваются в имя исходного файла и номер текущей строки в этом файле. В С они были доступны с самого начала. C99 определяет дополнительный предопределенный идентификатор, __func__
, который представляет имя текущей функции в виде символьной строки. Макросы используются следующим образом:
if (some_system_call(param1, param2) < 0) {
fprintf(stderr, "%s: %s (%s %d): some_system_call(%d, %d) failed: %sn",
argv[0], __func__, __FILE__, __LINE__,
param1, param2, strerror(errno));
return 1;
}
Здесь сообщение об ошибке включает не только имя программы, но также и имя функции, имя исходного файла и номер строки. Полный список идентификаторов, полезных для диагностики, приведен в табл. 4.2.
Таблица 4.2. Диагностические идентификаторы C99
__DATE__ | C89 | Дата компиляции в виде «Mmm nn yyyy » |
__FILE_ | Оригинальная | Имя исходного файла в виде «program.c » |
__LINE__ | Оригинальная | Номер строки исходного файла в виде 42 |
__TIME__ | C89 | Время компиляции в виде «hh:mm:ss » |
__func__ | C99 | Имя текущей функции, как если бы было объявлено const char __func__[] = "name" |
Использование __FILE__
и __LINE__
было вполне обычно для ранних дней Unix, когда у большинства людей были исходные коды и они могли находить ошибки и устранять их. По мере того, как системы Unix становились все более коммерческими, использование этих идентификаторов постепенно уменьшалось, поскольку знание положения в исходном коде дает немного пользы, когда имеется лишь двоичный исполняемый файл.
Сегодня, хотя системы GNU/Linux поставляются с исходными кодами, указанный исходный код часто не устанавливается по умолчанию. Поэтому использование этих идентификаторов для сообщений об ошибках не представляет дополнительной ценности. GNU Coding Standards даже не упоминает их.
4.4. Ввод и выводВсе операции Linux по вводу/выводу осуществляются посредством дескрипторов файлов. Данный раздел знакомит с дескрипторами файлов, описывает, как их получать и освобождать, и объясняет, как выполнять с их помощью ввод/вывод.
Дескриптор файла является целым значением. Действительные дескрипторы файлов начинаются с 0 и растут до некоторого установленного системой предела. Эти целые фактически являются индексами таблицы открытых файлов для каждого процесса (Таблица поддерживается внутри операционной системы; она недоступна запущенным программам.) В большинстве современных систем размеры таблиц большие. Команда 'ulimit -n
' печатает это значение:
$ ulimit -n
1024
Из С максимальное число открытых файлов возвращается функцией getdtablesize()
(получить размер таблицы дескрипторов):
#include
int getdtablesize(void);
Следующая небольшая программа выводит результат работы этой функции:
/* ch04-maxfds.с – Демонстрация getdtablesize(). */
#include
#include
int main(int argc, char **argv) {
printf("max fds: %dn", getdtablesize());
exit(0);
}
Неудивительно, что после компиляции и запуска эта программа выводит то же значение, что и ulimit
:
$ ch04-maxfds
max fds: 1024
Дескрипторы файлов содержатся в обычных переменных int
; для использования с системными вызовами ввода/вывода можно увидеть типичные объявления вида 'int fd
'. Для дескрипторов файлов нет предопределенного типа.
В обычном случае каждая программа начинает свою работу с тремя уже открытыми для нее дескрипторами файлов. Это стандартный ввод, стандартный вывод и стандартная ошибка, с дескрипторами файлов 0, 1 и 2 соответственно. (Если не было использовано перенаправление, каждый из них связан с клавиатурой и с экраном.)
Очевидные символические константы. Оксюморон?
При работе с системными вызовами на основе дескрипторов файлов и стандартных ввода, вывода и ошибки целые константы 0, 1 и 2 обычно используются прямо в коде. В подавляющем большинстве случаев использование таких символических констант (manifest constants) является плохой мыслью. Вы никогда не знаете, каково значение некоторой случайной целой константы и имеет ли к ней какое-нибудь отношение константа с тем же значением, использованная в другой части кода. С этой целью стандарт POSIX требует объявить следующие именованные константы (symbolic constants) в
:
STDIN_FILENO
«Номер файла» для стандартного ввода: 0.
STDOUT_FILENO
Номер файла для стандартного вывода: 1.
STDERR_FILENO
Номер файла для стандартной ошибки: 2.Однако, по нашему скромному мнению, использование этих макросов избыточно. Во-первых, неприятно набирать 12 или 13 символов вместо 1. Во-вторых, использование 0, 1 и 2 так стандартно и так хорошо известно, что на самом деле нет никаких оснований для путаницы в смысле этих конкретных символических констант.
С другой стороны, использование этих констант не оставляет сомнений в намерениях программиста. Сравните это утверждение:
int fd = 0;
Инициализируется ли
fd
значением стандартного ввода, или же программист благоразумно инициализирует свои переменные подходящим значением? Вы не можете этого сказать.Один из подходов (рекомендованный Джеффом Колье (Geoff Collyer)) заключается в использовании следующего определения
enum
:
enum { Stdin, Stdout, Stderr };
Затем эти константы можно использовать вместо 0, 1 и 2. Их легко читать и печатать.
Новые дескрипторы файлов получают (наряду с другими источниками) в результате системного вызова open()
. Этот системный вызов открывает файл для чтения или записи и возвращает новый дескриптор файла для последующих операций с этим файлом. Мы видели объявление раньше:
#include
#include
#include
#include
int open(const char *pathname, int flags, mode_t mode);
Три аргумента следующие:
const char *pathname
Строка С, представляющая имя открываемого файла.
int flags
Поразрядное ИЛИ с одной или более констант, определенных в
. Вскоре мы их рассмотрим.
mode_t mode
Режимы доступа для создаваемого файла. Это обсуждается далее в главе, см. раздел 4.6 «Создание файлов». При открытии существующего файла опустите этот параметр[46]46
open()
является одним из немногих варьирующих (variadic) системных вызовов – Примеч. автора.
[Закрыть].
Возвращаемое open() значение является либо новым дескриптором файла, либо -1, означающим ошибку, в этом случае будет установлена errno
. Для простого ввода/вывода аргумент flags
должен быть одним из значений из табл. 4.3.
Таблица 4.3. Значения flags
для open()
O_RDONLY | 0 | Открыть файл только для чтения, запись невозможны |
O_WRONLY | 1 | Открыть файл только для записи, чтение невозможно |
O_RDWR | 2 | Открыть файл для чтения и записи |
Вскоре мы увидим пример кода. Дополнительные значения flags
описаны в разделе 4.6 «Создание файлов». Большой объем ранее написанного кода Unix не использовал эти символические значения. Вместо этого использовались числовые значения. Сегодня это рассматривается как плохая практика, но мы представляем эти значения, чтобы вы их распознали, если встретитесь с ними
Системный вызов close()
закрывает файл: его элемент в системной таблице дескрипторов файлов помечается как неиспользуемый, и с этим дескриптором нельзя производить никаких дальнейших действий. Объявление следующее:
#include
int close(int fd);
В случае успеха возвращается 0, при ошибке (-1). При возникновении ошибки нельзя ничего сделать, кроме сообщения о ней. Ошибки при закрытии файлов являются необычными, но не невозможными, особенно для файлов, доступ к которым осуществляется через сеть. Поэтому хорошей практикой является проверка возвращаемого значения, особенно для файлов, открытых для записи.
Если вы будете игнорировать возвращаемое значение, специально приведите его к типу void
, чтобы указать, что вам не нужен результат:
(void)close(fd); /* отказ от возвращаемого значения */
Легкомысленность этого совета в том, что слишком большое количество приведений к void
имеют тенденцию загромождать код. Например, несмотря на принцип «всегда проверять возвращаемое значение», чрезвычайно редко можно увидеть код, проверяющий возвращаемое значение printf()
или приводящий его к void
. Как и со многими аспектами программирования на С, здесь также требуются опыт и рассудительность.
Как упоминалось, число открытых файлов, если оно большое, ограничивается, и вам всегда следует закрывать файлы, когда работа с ними закончена. Если вы этого не сделаете, то в конечном счете выйдете за пределы лимита дескрипторов файлов, создав ситуацию, которая ведет к потере устойчивости части вашей программы.
Система закрывает все открытые файлы, когда процесс завершается, но – за исключением 0, 1 и 2 – плохая манера полагаться на это.
Когда open()
возвращает новый дескриптор файла, она всегда возвращает наименьшее неиспользуемое целое значение. Всегда. Поэтому, если открыты дескрипторы файлов 0–6 и программа закрывает дескриптор файла 5, следующий вызов open()
вернет 5, а не 7. Это поведение важно; далее в книге мы увидим, как оно используется для аккуратной реализации многих важных особенностей Unix, таких, как перенаправление ввода/вывода и конвейеризация (piping)
FILE*
на дескрипторы файловСтандартные библиотечные функции ввода/вывода и переменные FILE*
из
, такие, как stdin
, stdout
и stderr
, построены поверх основанных на дескрипторах файлов системных вызовах.
Иногда полезно получить непосредственный доступ к дескриптору файла, связанному с указателем файла
, если вам нужно сделать что-либо, не определенное стандартом С ISO. Функция fileno()
возвращает лежащий в основе дескриптор файла:
#include
int fileno(FILE *stream);
Пример мы увидим позже, в разделе 4.4.4. «Пример: Unix cat».
Открытые файлы наследуются порожденными процессами от своих родительских процессов. Фактически они являются общими. В частности, общим является положение в файле. Подробности мы оставим для дальнейшего обсуждения в разделе 9.1.1.2 «Разделение дескрипторов файлов».
Поскольку программы могут наследовать другие файлы, иногда вы можете увидеть программы, которые закрывают все свои файлы, чтобы начать с «чистого состояния» В частности, типичен код наподобие этого:
int i;
/* оставить лишь 0, 1, и 2 */
for (i = 3; i < getdtablesize(); i++)
(void)close(i);
Предположим, что результат getdtablesize()
равен 1024. Этот код работает, но он делает (1024-3)*2 = 2042 системных вызова. 1020
из них не нужны, поскольку возвращаемое значение getdtablesize()
не изменяется. Вот лучший вариант этого кода:
int i, fds;
for (i = 3, fds = getdtablesize(); i < fds; i++)
(void)close(i);
Такая оптимизация не ухудшает читаемость кода, но может быть заметна разница, особенно на медленных системах. В общем, стоит поискать случаи, когда в циклах повторно вычисляется один и тот же результат, чтобы посмотреть, нельзя ли вынести вычисление за пределы цикла. Хотя в таких случаях нужно убедиться, что вы (а) сохраняете правильность кода и (б) сохраняете его читаемость!
Ввод/вывод осуществляется системными вызовами read()
и write()
соответственно:
#include
#include
#include
#include
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
Каждая функция сделана как можно проще. Аргументами являются дескриптор открытого файла, указатель на буфер для чтения или записи данных и число читаемых или записываемых байтов.
Возвращаемое значение является числом действительно прочитанных или записанных байтов. (Это число может быть меньше запрошенного: при операции чтения это происходит, когда в файле осталось меньше count
байтов, а при операции записи это случается, когда диск заполнен или произошла еще какая-нибудь ошибка.) Возвращаемое значение -1 означает возникшую ошибку, в этом случае errno указывает эту ошибку. Когда read()
возвращает 0, это означает, что достигнут конец файла.
Теперь мы можем показать оставшуюся часть кода для ch04-cat
. Процедура process()
использует 0 для стандартного ввода, если именем файла является «-
» (строки 50 и 51). В противном случае она открывает данный файл:
36 /*
37 * process – сделать что-то с файлом, в данном случае,
38 * послать его в stdout (fd 1).
39 * Возвращает 0, если все нормально; в противном случае 1.
40 */
41
42 int
43 process(char *file)
44 {
45 int fd;
46 ssize_t rcount, wcount;
47 char buffer[BUFSIZ];
48 int errors = 0;
49
50 if (strcmp(file, "-") == 0)
51 fd = 0;
52 else if ((fd = open(file, O_RDONLY)) < 0) {
53 fprintf(stderr, "%s: %s: cannot open for reading: %sn",
54 myname, file, strerror(errno));
55 return 1;
56 }
Буфер buffer
(строка 47) имеет размер BUFSIZ
; эта константа определена В
как «оптимальный» размер блока для ввода/вывода. Хотя значение BUFSIZ
различается в разных системах, код, использующий эту константу, чистый и переносимый.
Основой процедуры является следующий цикл, который повторно читает данные до тех пор, пока не будет достигнут конец файла или не возникнет ошибка.
58 while ((rcount = read(fd, buffer, sizeof buffer)) > 0) {
59 wcount = write(1, buffer, rcount);
60 if (wcount != rcount) {
61 fprintf(stderr, "%s: %s: write error: %sn",
62 myname, file, strerror(errno));
63 errors++;
64 break;
65 }
66 }
Переменные rcount
и wcount
(строка 45) имеют тип ssize_t
, «знаковый size_t
», который позволяет хранить в них отрицательные значения. Обратите внимание, что число байтов, переданное write()
, является значением, возвращенным read()
(строка 59). Хотя мы хотим читать порциями фиксированного размера в BUFSIZ
, маловероятно, что размер самого файла кратен BUFSIZ
. При чтении из файла завершающей, меньшей порции байтов, возвращаемое значение указывает, сколько байтов buffer получили новые данные. В стандартный вывод должны быть скопированы только эти байты, а не весь буфер целиком.
Условие 'wcount != rcount
' в строке 60 является правильным способом проверки на ошибки; если были записаны некоторые, но не все данные, wcount
будет больше нуля, но меньше rcount
.
В заключение process()
проверяет наличие ошибок чтения (строки 68–72), а затем пытается закрыть файл. В случае (маловероятном) неудачного завершения close()
(строка 75) она выводит сообщение об ошибке. Избежание закрытия стандартного ввода не является абсолютно необходимым в данной программе, но является хорошей привычкой при разработке больших программ, в случае, когда другой код где-то в другом месте хочет что-то с ним делать или если порожденная программа будет наследовать его. Последний оператор (строка 82) возвращает 1, если были ошибки, и 0 в противном случае.
68 if (rcount < 0) {
69 fprintf(stderr, "%s: %s: read error: %sn",
70 myname, file, strerror(errno));
71 errors++;
72 }
73
74 if (fd != 0) {
75 if (close(fd) < 0) {
76 fprintf(stderr, "%s: %s: close error: %sn",
77 myname, file, strerror(errno));
78 errors++;
79 }
80 }
81
82 return (errors != 0);
83 }
ch04-cat
проверяет на ошибки каждый системный вызов. Хотя это утомительно, зато предоставляет устойчивость (или по крайней мере, ясность): когда что-то идет не так, ch04-cat
выводит сообщение об ошибке, которое специфично настолько, насколько это возможно. В сочетании с errno
и strerror()
это просто. Вот все с ch04-cat
, всего 88 строк кода!
Для подведения итогов вот несколько важных моментов, которые нужно понять относительно ввода/вывода в Unix:
Ввод/вывод не интерпретируется
Системные вызовы ввода/вывода просто перемешают байты. Они не интерпретируют данные; вся интерпретация оставлена программе уровня пользователя. Это делает чтение и запись двоичных структур таким же простым, как чтение и запись строк текста (на самом деле, проще, хотя использование двоичных данных привносит проблемы переносимости).
Ввод/вывод гибок
За один раз вы можете прочесть или записать столько байтов, сколько захотите. Вы можете даже читать или записывать данные по одному байту за раз, хотя для больших объемов данных это обходится дороже, чем использование больших порций.
Ввод/вывод прост
Три уровня возвращаемых значений (отрицательные для ошибок, ноль для конца файла и положительные для счета) делают программирование простым и очевидным.
Ввод/вывод может быть частичным
Как read()
, так и write()
могут переместить меньше байтов, чем запрошено. Код приложения (т.е. ваш код) всегда должен учитывать это.