Skip to content

Latest commit

 

History

History
236 lines (175 loc) · 11 KB

File metadata and controls

236 lines (175 loc) · 11 KB

9. Обработка ошибок в системных вызовах

Большинство системных вызовов (например работа с файлами) возвращают код завершения, впрочем, как и любые другие, хорошо сделанные программы. А как иначе понять успешно завершилось или нет? Поэтому возвращается либо 0 - успех, либо другое число, например -1 или 1.

Однако с этим числом, кроме бинарного понимания "успех/сбой" ничего не поймешь.

Когда мы получили сбой, получив, например -1, хотелось бы узнать что конкретно произошло.

Для диагностирования проблемы, существует специальная переменная errno, ей присваиваются специальные коды ошибок, например для ошибки, что файл уже существует есть константа со значением EEXIST=17.

А где узнать значения ошибок?

Узнать их можно использовав справочник:

errno -l

Или же получить значение ошибки в строковую переменную, через функцию strerror() из заголовка <string.h>:

char* error_string = strerror(errno);

Программа

create_file

Что делает

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

Пример выполнения

Обычное создание пустого файла:

./create_file.out kekfile.txt

Ошибка повторного создания файла:

./create_file.out kekfile.txt
file creation failed.
file already exists!
strerror() result: File exists

Ошибка создания файла в директории, где нет прав на создание (корневая директория /) :

./create_file.out /kekfile.txt
file creation failed.
permission denied!
strerror() result: Permission denied

Что внутри

  • использование системного вызова open() для создания файла.
  • определение ошибок и вывод их на экран.

Исходный код

public/code_sources/linux-c-system-programming-essentials/8-system-calls/error_handling/create_file.c

Ссылка

Пояснения

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

int open_code = open(argv[1], O_CREAT | O_EXCL | O_WRONLY, 0644);

Функция open() в данном случае принимает три параметра:

  • filename - путь к файлу, который нужно создать, например /home/documents/hello.txt .
  • O_CREAT | O_EXCL | O_WRONLY - три флага, указывающих что делать при вызове open() .

O_CREAT
Указывает на необходиомость создания файла, если он еще не существует.

O_EXCL
Эксклюзивность, работаем только если файла еще нет.

O_WRONLY
Открыть файл только для записи.

  • 0644 - права доступа к созданному файлу.

6 = 4+2 = rw- (чтение + запись для владельца)
4 = r-- (чтение для группы)
4 = r-- (чтение для остальных)

Итого, файл будет доступен для записи текущему пользователю.

Проверка и обработка ошибок

Сперва проверяем, что произошел сбой у вызова open():

if(open_code == -1)

Если сбой произошел, то уже можно проверять конкретные ошибки из заголовка <errno.h>:

if(errno == EACCES)
{
    fprintf(stderr, "permission denied!\n");
}
else if(errno == EEXIST)
{
    fprintf(stderr, "file already exists!\n");
}
else 
{
    fprintf(stderr, "unknown errno: %d\n", errno);
}

Как видим, мы можем четко определить что за ошибка, сравнивая переменную errno с соответствующими константами из заголовка <errno.h>.

Если мы хотим получить строковое представление текущей ошибки из errno , то существует функция strerror() из заголовка <string.h>:

fprintf( stderr, "strerror() result: %s\n", strerror(errno) );

Таким образом, мы можем преобразовать числовую константу из списка ошибок в понятную строку, например, если errno имеет значение 17 равное константе EEXIST, то получим следующую строку:

"File exists"

Еще есть функция perror(), которая сразу же пишет сообщение об ошибке. Внутри себя она использует переменную errno и выводит ее текстовое представление в вывод stderr.

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

10. Использование потоков данных (streams) для записи в файл

Следует не путать потоки данных (stream) и потоки выполнения (thread).

В этом разделе мы будем работать с потоками данных (streams), далее я буду называть их стримами.

stream

Это высокоуровневая абстракция, построенная над файловыми дескрипторами , предоставляет удобный интерфейс для работы с потоками данных, для чтения/записи в файлы (в Linux все является файлами!).
Стримы в языке Си предоставлены структурой FILE из заголовка <stdio.h>.

Программа

file_stream_write

Что делает

Принимает текст от пользователя и записывает его в файл.

Пример выполнения

./file_stream_write.out goga.txt

giga 
boy
hi
how are you doing?

# CTRL + D чтобы закончить ввод

# читаем получившийся файл:
cat goga.txt

giga
boy
hi
how are you doing?

Что внутри

  • Использование стримов для ввода пользователя и вывода в файл.

Исходный код

public/code_sources/linux-c-system-programming-essentials/8-system-calls/file_streams/file_stream_write.c

Ссылка

Пояснения

Запись ввода пользователя в файл

Объявляем переменную типа FILE* и строковый буфер для считывания ввода пользователя:

FILE* file = NULL;

char line_buffer[LINE_BUFFER_SIZE] = { 0 };

Затем открываем файл через функцию fopen():

file = fopen(argv[1], "w");

Далее получаем ввод от пользователя, записываем его ввод в файл:

while( fgets( line_buffer, sizeof(line_buffer), stdin ) != NULL )
{
        fprintf( file, line_buffer );
}

И закрываем файл в конце программы:

fprintf( file, line_buffer );

Возможность делать вывод в двоичном формате

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

file = fopen(argv[1], "wb")

А запись файла делать с помощью функции fwrite(), она делает ввод двоичным, и имеет такую сигнатуру:

size_t fwrite(const void ptr[restrict .size * .nmemb],
                   size_t size, size_t nmemb,
                   FILE *restrict stream);

Сравнение с использованием дескрипторов файлов

Изначально, до появления стримов, подобные операции делались с помощью вызовов open() , read() и write(), все они работают с файловыми дескрипторами целочисленного типа int. Из минусов такого подхода можно описать следующее:

  1. Больше кода - вам нужно создавать множество буферов, делать больше вызовов. Больше кода = больше ошибок. Более того, функциями ввода/вывода стримов вроде fprintf() и fgets() удобнее пользоваться, вместо ручного копирования байтов вручную.
  2. Отсутствие буферизации - в объект FILE уже встроена буферизация, вам не нужно создавать ее самому.
  3. Производительность - внутри операций с FILE вроде fopen() и fprintf() происходит меньше системных вызовов ввода/вывода, из-за встроенной буферизации.

Следующая статья