Include unistd h что это
В языке C для осуществления файлового ввода-вывода используются механизмы стандартной библиотеки языка, объявленные в заголовочном файле stdio.h. Как вы вскоре узнаете консольный ввод-вывод — это не более чем частный случай файлового ввода-вывода. В C++ для ввода-вывода чаще всего используются потоковые типы данных. Однако все эти механизмы являются всего лишь надстройками над низкоуровневыми механизмами ввода-вывода ядра операционной системы.
С точки зрения модели КИС (Клиент-Интерфейс-Сервер), сервером стандартных механизмов ввода вывода языка C (printf, scanf, FILE*, fprintf, fputc и т. д.) является библиотека языка. А сервером низкоуровневого ввода-вывода в Linux, которому посвящена эта глава книги, является само ядро операционной системы.
Пользовательские программы взаимодействуют с ядром операционной системы посредством специальных механизмов, называемых системными вызовами (system calls, syscalls). Внешне системные вызовы реализованы в виде обычных функций языка C, однако каждый раз вызывая такую функцию, мы обращаемся непосредственно к ядру операционной системы. Список всех системных вызовов Linux можно найти в файле /usr/include/asm/unistd.h. В этой главе мы рассмотрим основные системные вызовы, осуществляющие ввод-вывод: open(), close(), read(), write(), lseek() и некоторые другие.
5.2. Файловые дескрипторы
В языке C при осуществлении ввода-вывода мы используем указатель FILE*. Даже функция printf() в итоге сводится к вызову vfprintf(stdout. ), разновидности функции fprintf(); константа stdout имеет тип struct _IO_FILE*, синонимом которого является тип FILE*. Это я к тому, что консольный ввод-вывод — это файловый ввод-вывод. Стандартный поток ввода, стандартный поток вывода и поток ошибок (как в C, так и в C++) — это файлы. В Linux все, куда можно что-то записать или откуда можно что-то прочитать представлено (или может быть представлено) в виде файла. Экран, клавиатура, аппаратные и виртуальные устройства, каналы, сокеты — все это файлы. Это очень удобно, поскольку ко всему можно применять одни и те же механизмы ввода-вывода, с которыми мы и познакомимся в этой главе. Владение механизмами низкоуровневого ввода-вывода дает свободу перемещения данных в Linux. Работа с локальными файловыми системами, межсетевое взаимодействие, работа с аппаратными устройствами, — все это осуществляется в Linux посредством низкоуровневого ввода-вывода.
Вы уже знаете из предыдущей главы, что при запуске программы в системе создается новый процесс (здесь есть свои особенности, о которых пока говорить не будем). У каждого процесса (кроме init) есть свой родительский процесс (parent process или просто parent), для которого новоиспеченный процесс является дочерним (child process, child). Каждый процесс получает копию окружения (environment) родительского процесса. Оказывается, кроме окружения дочерний процесс получает в качестве багажа еще и копию таблицы файловых дескрипторов.
Файловый дескриптор (file descriptor) — это целое число (int), соответствующее открытому файлу. Дескриптор, соответствующий реально открытому файлу всегда больше или равен нулю. Копия таблицы дескрипторов (читай: таблицы открытых файлов внутри процесса) скрыта в ядре. Мы не можем получить прямой доступ к этой таблице, как при работе с окружением через environ. Можно, конечно, кое-что «вытянуть» через дерево /proc, но нам это не надо. Программист должен лишь понимать, что каждый процесс имеет свою копию таблицы дескрипторов. В пределах одного процесса все дескрипторы уникальны (даже если они соответствуют одному и тому же файлу или устройству). В разных процессах дескрипторы могут совпадать или не совпадать — это не имеет никакого значения, поскольку у каждого процесса свой собственный набор открытых файлов.
Возникает вопрос: сколько файлов может открыть процесс? В каждой системе есть свой лимит, зависящий от конфигурации. Если вы используете bash или ksh (Korn Shell), то можете воспользоваться внутренней командой оболочки ulimit, чтобы узнать это значение.
$ ulimit -n 1024 $
Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit:
$ limit descriptors descriptors 1024 $
В командной оболочке, в которой вы работаете (bash, например), открыты три файла: стандартный ввод (дескриптор 0), стандартный вывод (дескриптор 1) и стандартный поток ошибок (дескриптор 2). Когда под оболочкой запускается программа, в системе создается новый процесс, который является для этой оболочки дочерним процессом, следовательно, получает копию таблицы дескрипторов своего родителя (то есть все открытые файлы родительского процесса). Таким образом программа может осуществлять консольный ввод-вывод через эти дескрипторы. На протяжении всей книги мы будем часто играть с этими дескрипторами.
Таблица дескрипторов, помимо всего прочего, содержит информацию о текущей позиции чтения-записи для каждого дескриптора. При открытии файла, позиция чтения-записи устанавливается в ноль. Каждый прочитанный или записанный байт увеличивает на единицу указатель текущей позиции. Мы вернемся к этой теме в разделе 5.7.
5.3. Открытие файла: системный вызов open()
Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов open(). Этот системный вызов не имеет постоянного списка аргументов (за счет использования механизма va_arg); в связи с этим существуют две «разновидности» open(). Не только в С++ есть перегрузка функций 😉 Если интересно, то о механизме va_arg можно прочитать на man-странице stdarg (man 3 stdarg) или в книге Б. Кернигана и Д. Ритчи «Язык программирования Си». Ниже приведены адаптированные прототипы системного вызова open().
int open (const char * filename, int flags, mode_t mode); int open (const char * filename, int flags);
Системный вызов open() объявлен в заголовочном файле fcntl.h. Ниже приведен общий адаптированный прототип open().
int open (const char * filename, int flags, . );
Начнем по порядку. Первый аргумент — имя файла в файловой системе в обычной форме: полный путь к файлу (если файл не находится в текущем каталоге) или сокращенное имя (если файл в текущем каталоге).
Второй аргумент — это режим открытия файла, представляющий собой один или несколько флагов открытия, объединенных оператором побитового ИЛИ. Список доступных флагов приведен в Таблице 4 Приложения 2.. Наиболее часто используют только первые семь флагов. Если вы хотите, например, открыть файл в режиме чтения и записи, и при этом автоматически создать файл, если такового не существует, то второй аргумент open() будет выглядеть примерно так: O_RDWR|O_CREAT. Константы-флаги открытия объявлены в заголовочном файле bits/fcntl.h, однако не стоит включать этот файл в свои программы, поскольку он уже включен в файл fcntl.h.
Третий аргумент используется в том случае, если open() создает новый файл. В этом случае файлу нужно задать права доступа (режим), с которыми он появится в файловой системе. Права доступа задаются перечислением флагов, объединенных побитовым ИЛИ. Вместо флагов можно использовать число (как правило восьмиричное), однако первый способ нагляднее и предпочтительнее. Список флагов приведен в Таблице 1 Приложения 2. Чтобы, например, созданный файл был доступен в режиме «чтение-запись» пользователем и группой и «только чтение» остальными пользователями, — в третьем аргументе open() надо указать примерно следующее: S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH или 0664. Флаги режима доступа реально объявлены в заголовочном файле bits/stat.h, но он не предназначен для включения в пользовательские программы, и вместо него мы должны включать файл sys/stat.h. Тип mode_t объявлен в заголовочном файле sys/types.h.
Если файл был успешно открыт, open() возвращает файловый дескриптор, по которому мы будем обращаться к файлу. Если произошла ошибка, то open() возвращает -1. Позже, в последующих главах книги мы научимся распознавать ошибки системных вызовов.
5.4. Закрытие файла: системный вызов close()
Системный вызов close() закрывает файл. Вообще говоря, по завершении процесса все открытые файлы (кроме файлов с дескрипторами 0, 1 и 2) автоматически закрываются. Тем не менее, это не освобождает нас от самостоятельного вызова close(), когда файл нужно закрыть. К тому же, если файлы не закрывать самостоятельно, то соответствующие дескрипторы не освобождаются, что может привести к превышению лимита открытых файлов. Простой пример: приложение может быть настроено так, чтобы каждую минуту открывать и перечитывать свой файл конфигурации для проверки обновлений. Если каждый раз файл не будет закрываться, то в моей системе, например, приложение может «накрыться медным тазом» примерно через 17 часов. Автоматически! Кроме того, файловая система Linux поддерживает механизм буферизации. Это означает, что данные, которые якобы записываются, реально записываются на носитель (синхронизируются) только через какое-то время, когда система сочтет это правильным и оптимальным. Это повышает производительность системы и даже продлевает ресурс жестких дисков. Системный вызов close() не форсирует запись данных на диск, однако дает больше гарантий того, что данные останутся в целости и сохранности.
Системный вызов close() объявлен в файле unistd.h. Ниже приведен его адаптированный прототип.
int close (int fd);
Очевидно, что единственный аргумент — это файловый дескриптор. Возвращаемое значение — ноль в случае успеха, и -1 — в случае ошибки. Довольно часто close() вызывают без проверки возвращаемого значения. Это не очень грубая ошибка, но, тем не менее, иногда закрытие файла бывает неудачным (в случае неправильного дескриптора, в случае прерывания функции по сигналу или в случае ошибки ввода-вывода, например). В любом случае, если программа сообщит пользователю, что файл невозможно закрыть, это хорошо.
Теперь можно написать простенкую программу, использующую системные вызовы open() и close(). Мы еще не умеем читать из файлов и писать в файлы, поэтому напишем программу, которая создает файл с именем, переданным в качестве аргумента (argv[1]) и с правами доступа 0600 (чтение и запись для пользователя). Ниже приведен исходный код программы.
/* openclose.c */ #include /* open() and O_XXX flags */ #include /* S_IXXX flags */ #include /* mode_t */ #include /* close() */ #include #include int main (int argc, char ** argv) < int fd; mode_t mode = S_IRUSR | S_IWUSR; int flags = O_WRONLY | O_CREAT | O_EXCL; if (argc < 2) < fprintf (stderr, "openclose: Too few arguments\n"); fprintf (stderr, "Usage: openclose \n"); exit (1); > fd = open (argv[1], flags, mode); if (fd < 0) < fprintf (stderr, "openclose: Cannot open file '%s'\n", argv[1]); exit (1); >if (close (fd) != 0) < fprintf (stderr, "Cannot close file (descriptor=%d)\n", fd); exit (1); >exit (0); >
Обратите внимание, если запустить программу дважды с одним и тем же аргументом, то на второй раз open() выдаст ошибку. В этом виноват флаг O_EXCL (см. Таблицу 4 Приложения 2), который «дает добро» только на создание еще не существующих файлов. Наглядности ради, флаги открытия и флаги режима мы занесли в отдельные переменные, однако можно было бы сделать так:
fd = open (argv[1], O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
fd = open (argv[1], O_WRONLY | O_CREAT | O_EXCL, 0600);
5.5. Чтение файла: системный вызов read()
Системный вызов read(), объявленный в файле unistd.h, позволяет читать данные из файла. В отличие от библиотечных функций файлового ввода-вывода, которые предоставляют возможность интерпретации считываемых данных. Можно, например, записать в файл следующее содержимое:
2006
Теперь, используя библиотечные механизмы, можно читать файл по-разному:
fscanf (filep, "%s", buffer); fscanf (filep, "%d", number);
Системный вызов read() читает данные в «сыром» виде, то есть как последовательность байт, без какой-либо интерпретации. Ниже представлен адаптированный прототип read().
ssize_t read (int fd, void * buffer, size_t count);
Первый аргумент — это файловый дескриптор. Здесь больше сказать нечего. Второй аргумент — это указатель на область памяти, куда будут помещаться данные. Третий аргумент — количество байт, которые функция read() будет пытаться прочитать из файла. Возвращаемое значение — количество прочитанных байт, если чтение состоялось и -1, если произошла ошибка. Хочу заметить, что если read() возвращает значение меньше count, то это не символизирует об ошибке.
Хочу сказать несколько слов о типах. Тип size_t в Linux используется для хранения размеров блоков памяти. Какой тип реально скрывается за size_t, зависит от архитектуры; как правило это unsigned long int или unsigned int. Тип ssize_t (Signed SIZE Type) — это тот же size_t, только знаковый. Используется, например, в тех случаях, когда нужно сообщить об ошибке, вернув отрицательный размер блока памяти. Системный вызов read() именно так и поступает.
Теперь напишем программу, которая просто читает файл и выводит его содержимое на экран. Имя файла будет передаваться в качестве аргумента (argv[1]). Ниже приведен исходный код этой программы.
/* myread.c */ #include #include #include #include #include #include int main (int argc, char ** argv) < int fd; ssize_t ret; char ch; if (argc < 2) < fprintf (stderr, "Too few arguments\n"); exit (1); >fd = open (argv[1], O_RDONLY); if (fd < 0) < fprintf (stderr, "Cannot open file\n"); exit (1); >while ((ret = read (fd, &ch, 1)) > 0) < putchar (ch); >if (ret < 0) < fprintf (stderr, "myread: Cannot read file\n"); exit (1); >close (fd); exit (0); >
В этом примере используется укороченная версия open(), так как файл открывается только для чтения. В качестве буфера (второй аргумент read()) мы передаем адрес переменной типа char. По этому адресу будут считываться данные из файла (по одному байту за раз) и передаваться на стандартный вывод. Цикл чтения файла заканчивается, когда read() возвращает нуль (нечего больше читать) или -1 (ошибка). Системный вызов close() закрывает файл.
Как можно заметить, в нашем примере системный вызов read() вызывается ровно столько раз, сколько байт содержится в файле. Иногда это действительно нужно; но не здесь. Чтение-запись посимвольным методом (как в нашем примере) значительно замедляет процесс ввода-вывода за счет многократных обращений к системным вызовам. По этой же причине возрастает вероятность возникновения ошибки. Если нет действительной необходимости, файлы нужно читать блоками. О том, какой размер блока предпочтительнее, будет рассказано в последующих главах книги. Ниже приведен исходный код программы, которая делает то же самое, что и предыдущий пример, но с использованием блочного чтения файла. Размер блока установлен в 64 байта.
/* myread1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 64 int main (int argc, char ** argv) < int fd; ssize_t read_bytes; char buffer[BUFFER_SIZE+1]; if (argc < 2) < fprintf (stderr, "Too few arguments\n"); exit (1); >fd = open (argv[1], O_RDONLY); if (fd < 0) < fprintf (stderr, "Cannot open file\n"); exit (1); >while ((read_bytes = read (fd, buffer, BUFFER_SIZE)) > 0) < buffer[read_bytes] = 0; /* Null-terminator for C-string */ fputs (buffer, stdout); >if (read_bytes < 0) < fprintf (stderr, "myread: Cannot read file\n"); exit (1); >close (fd); exit (0); >
Теперь можно примерно оценить и сравнить скорость работы двух примеров. Для этого надо выбрать в системе достаточно большой файл (бинарник ядра или видеофильм, например) и посмотреть на то, как быстро читаются эти файлы:
$ time ./myread /boot/vmlinuz > /dev/null real 0m1.443s user 0m0.383s sys 0m1.039s $ time ./myread1 /boot/vmlinuz > /dev/null real 0m0.055s user 0m0.010s sys 0m0.023s $
5.6. Запись в файл: системный вызов write()
Для записи данных в файл используется системный вызов write(). Ниже представлен его прототип.
ssize_t write (int fd, const void * buffer, size_t count);
Как видите, прототип write() отличается от read() только спецификатором const во втором аргументе. В принципе write() выполняет процедуру, обратную read(): записывает count байтов из буфера buffer в файл с дескриптором fd, возвращая количество записанных байтов или -1 в случае ошибки. Так просто, что можно сразу переходить к примеру. За основу возьмем программу myread1 из предыдущего раздела.
/* rw.c */ #include #include #include /* read(), write(), close() */ #include /* open(), O_RDONLY */ #include /* S_IRUSR */ #include /* mode_t */ #define BUFFER_SIZE 64 int main (int argc, char ** argv) < int fd; ssize_t read_bytes; ssize_t written_bytes; char buffer[BUFFER_SIZE]; if (argc < 2) < fprintf (stderr, "Too few arguments\n"); exit (1); >fd = open (argv[1], O_RDONLY); if (fd < 0) < fprintf (stderr, "Cannot open file\n"); exit (1); >while ((read_bytes = read (fd, buffer, BUFFER_SIZE)) > 0) < /* 1 == stdout */ written_bytes = write (1, buffer, read_bytes); if (written_bytes != read_bytes) < fprintf (stderr, "Cannot write\n"); exit (1); >> if (read_bytes < 0) < fprintf (stderr, "myread: Cannot read file\n"); exit (1); >close (fd); exit (0); >
В этом примере нам уже не надо изощеряться в попытках вставить нуль-терминатор в строку для записи, поскольку системный вызов write() не запишет большее количество байт, чем мы ему указали. В данном случае для демонстрации write() мы просто записывали данные в файл с дескриптором 1, то есть в стандартный вывод. Но прежде, чем переходить к чтению следующего раздела, попробуйте самостоятельно записать что-нибудь (при помощи write(), естественно) в обычный файл. Когда будете открывать файл для записи, обратите пожалуйста внимание на флаги O_TRUNC, O_CREAT и O_APPEND. Подумайте, все ли флаги сочетаются между собой по смыслу.
5.7. Произвольный доступ: системный вызов lseek()
Как уже говорилось, с каждым открытым файлом связано число, указывающее на текущую позицию чтения-записи. При открытии файла позиция равна нулю. Каждый вызов read() или write() увеличивает текущую позицию на значение, равное числу прочитанных или записанных байт. Благодаря этому механизму, каждый повторный вызов read() читает следующие данные, и каждый повторный write() записывает данные в продолжение предыдущих, а не затирает старые. Такой механизм последовательного доступа очень удобен, однако иногда требуется получить произвольный доступ к содержимому файла, чтобы, например, прочитать или записать файл заново.
Для изменения текущей позиции чтения-записи используется системный вызов lseek(). Ниже представлен его прототип.
off_t lseek (int fd, ott_t offset, int against);
Первый аргумент, как всегда, — файловый дескриптор. Второй аргумент — смещение, как положительное (вперед), так и отрицательное (назад). Третий аргумент обычно передается в виде одной из трех констант SEEK_SET, SEEK_CUR и SEEK_END, которые показывают, от какого места отсчитывается смещение. SEEK_SET — означает начало файла, SEEK_CUR — текущая позиция, SEEK_END — конец файла. Рассмотрим следующие вызовы:
lseek (fd, 0, SEEK_SET); lseek (fd, 20, SEEK_CUR); lseek (fd, -10, SEEK_END);
Первый вызов устанавливает текущую позицию в начало файла. Второй вызов смещает позицию вперед на 20 байт. В третьем случае текущая позиция перемещается на 10 байт назад относительно конца файла.
В случае удачного завершения, lseek() возвращает значение установленной «новой» позиции относительно начала файла. В случае ошибки возвращается -1.
Я долго думал, какой бы пример придумать, чтобы продемонстрировать работу lseek() наглядным образом. Наиболее подходящим примером мне показалась идея создания программы рисования символами. Программа оказалась не слишком простой, однако если вы сможете разобраться в ней, то можете считать, что успешно овладели азами низкоуровневого ввода-вывода Linux. Ниже представлен исходный код этой программы.
/* draw.c */ #include #include #include #include #include #include #include /* memset() */ #define N_ROWS 15 /* Image height */ #define N_COLS 40 /* Image width */ #define FG_CHAR 'O' /* Foreground character */ #define IMG_FN "image" /* Image filename */ #define N_MIN(A,B) ((A)<(B)?(A):(B)) #define N_MAX(A,B) ((A)>(B)?(A):(B)) static char buffer[N_COLS]; void init_draw (int fd) < ssize_t bytes_written = 0; memset (buffer, ' ', N_COLS); buffer [N_COLS] = '\n'; while (bytes_written < (N_ROWS * (N_COLS+1))) bytes_written += write (fd, buffer, N_COLS+1); >void draw_point (int fd, int x, int y) < char ch = FG_CHAR; lseek (fd, y * (N_COLS+1) + x, SEEK_SET); write (fd, &ch, 1); >void draw_hline (int fd, int y, int x1, int x2) < size_t bytes_write = abs (x2-x1) + 1; memset (buffer, FG_CHAR, bytes_write); lseek (fd, y * (N_COLS+1) + N_MIN (x1, x2), SEEK_SET); write (fd, buffer, bytes_write); >void draw_vline (int fd, int x, int y1, int y2) < int i = N_MIN(y1, y2); while (i int main (void) < int a, b, c, i = 0; char ch; int fd = open (IMG_FN, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) < fprintf (stderr, "Cannot open file\n"); exit (1); >init_draw (fd); char * icode[] = < "v 1 1 11", "v 11 7 11", "v 14 5 11", "v 18 6 11", "v 21 5 10", "v 25 5 10", "v 29 5 6", "v 33 5 6", "v 29 10 11", "v 33 10 11", "h 11 1 8", "h 5 16 17", "h 11 22 24", "p 11 5 0", "p 15 6 0", "p 26 11 0", "p 30 7 0", "p 32 7 0", "p 31 8 0", "p 30 9 0", "p 32 9 0", NULL >; while (icode[i] != NULL) < sscanf (icode[i], "%c %d %d %d", &ch, &a, &b, &c); switch (ch) < case 'v': draw_vline (fd, a, b, c); break; case 'h': draw_hline (fd, a, b, c); break; case 'p': draw_point (fd, a, b); break; default: abort(); >i++; > close (fd); exit (0); >
Теперь разберемся, как работает эта программа. Изначально «полотно» заполняется пробелами. Функция init_draw() построчно записывает в файл пробелы, чтобы получился «холст», размером N_ROWS на N_COLS. Массив строк icode в функции main() — это набор команд рисования. Команда начинается с одной из трех литер: ‘v’ — нарисовать вертикальную линию, ‘h’ — нарисовать горизонтальную линию, ‘p’ — нарисовать точку. После каждой такой литеры следуют три числа. В случае вертикальной линии первое число — фиксированная координата X, а два других числа — это начальная и конечная координаты Y. В случае горизонтальной линии фиксируется координата Y (первое число). Два остальных числа — начальная координата X и конечная координата X. При рисовании точки используются только два первых числа: координата X и координата Y. Итак, функция draw_vline() рисует вертикальную линию, функция draw_hline() рисует горизонтальную линию, а draw_point() рисует точку.
Функция init_draw() пишет в файл N_ROWS строк, каждая из которых содержит N_COLS пробелов, заканчивающихся переводом строки. Это процедура подготовки «холста».
Функция draw_point() вычисляет позицию (исходя из значений координат), перемещает туда текущую позицию ввода-вывода файла, и записывает в эту позицию символ (FG_CHAR), которым мы рисуем «картину».
Функция draw_hline() заполняет часть строки символами FG_CHAR. Так получается горизонтальная линия. Функция draw_vline() работает иначе. Чтобы записать вертикальную линию, нужно записывать по одному символу и каждый раз «перескакивать» на следующую строку. Эта функция работает медленнее, чем draw_hline(), но иначе мы не можем.
Полученное изображение записывается в файл image. Будьте внимательны: чтобы разгрузить исходный код, из программы исключены многие проверки (read(), write(), close(), диапазон координат и проч.). Попробуйте включить эти проверки самостоятельно.
Unix2017b/Ввод-вывод при помощи POSIX C API
Библиотека libc реализует как стандартные C-функции типа strcpy(), так и POSIX-функции типа getpid().
Как работают системные вызовы, мы рассматривали кратко на курсе C. Нельзя просто так вызвать функцию из ядра, как обычную функцию. Используется тот или иной механизм перехода в режим ядра в зависимости от архитектуры компьютера.
Многие POSIX-функции в libc являются тонкими обёртками над системными вызовами. Не каждая POSIX-функция является системным вызовом. Так и наоборот, не для каждого системного вызова есть соответствующая C-функция.
Библиотека libc является одним файлом (динамическая so и статическая a), размещается в каталоге /usr/lib. Кроме того, в состав glibc (GNU libc) входят ещё несколько библиотек:
- libm — математическая библиотека
- libpthread — POSIX Threads
Всего функций много, мы рассмотрим только несколько.
Использование из C
Заголовочный файл unistd.h — основной, его наличие и содержимое обеспечивается стандартом POSIX.1.
Типы данных
Примеры типов данных POSIX (тем не менее, они не являются стандартными типами в языке C).
- pid_t — идентификаторы процессов.
- ssize_t — аналогичен size_t, но обязан быть знаковым.
- off_t и off64_t — знаковый тип для хранения смещения в файле.
Файловый дескриптор
Как мы уже ранее обсуждали, файловый дескриптор — неотрицательное целое число (int).
POSIX определяет константы для потоков ввода-вывода, рекомендуется использовать их вместо магических чисел.
/* Standard file descriptors. */ #define STDIN_FILENO 0 /* Standard input. */ #define STDOUT_FILENO 1 /* Standard output. */ #define STDERR_FILENO 2 /* Standard error output. */
Функции ввода/вывода
Для большинства задач ввода-вывода достаточно всего четырёх функций: open, read, write, lseek, close.
Ввод-вывод является небуферизованным в том смысле, что каждый вызов read, write делает системный вызов в ядро ОС. Это важное отличие от стандартных C-функций printf, fwrite, .
Дёргать напрямую read/write для чтения/записи нескольких байт получается медленно. В этом случае обычно приходится реализовывать свою буферизацию.
open
#include #include #include int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int creat(const char *pathname, mode_t mode); int openat(int dirfd, const char *pathname, int flags); int openat(int dirfd, const char *pathname, int flags, mode_t mode);
Флаги определяют режим открытия файла, могут комбинироваться битовым или. Флагов много, можно найти в документации.
- O_RDONLY — открыть только для чтения
- O_WRONLY — открыть только для записи
- O_RDWR — открыть для чтения и записи
- O_APPEND — перед каждой записью позиция в файле ставится на конец и производится дописывание в конец
- O_TRUNC — если файл обычный, его длина будет вначале установлена в 0 (перезапись)
Функции openat принимают файловый дескриптор каталога и позволяют использовать путь относительно этого каталога (а не относительно текущего рабочего каталога программы).
creat
int creat(const char *pathname, mode_t mode);
Создание нового файла. Эквивалентно
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
close
#include int close(int fd);
Возвращает 0 при успехе, −1 при ошибке.
Отсутствие проверки возвращаемого значения close — частая ошибка. Может быть так, что об ошибке во время последнего write система сообщит пользователю только при close, в итоге данные могут быть молча потеряны.
Когда процесс завершается, все открытые файлы будут автоматически закрыты ядром.
Успешный вызов close не гарантирует, что данные физически записались на диск. Файловые системы обычно не занимаются сбросом своих буферов при close. Чтобы быть уверенным, нужно вызывать fsync.
read
ssize_t read(int fd, void *buf, size_t count);
read() пытается считывать count байтов из файлового дескриптора fd в буфер, начинающийся с buf.
- В файлах, которые поддерживают произвольный доступ, операция чтения начинается с текущего смещения, а смещение файла увеличивается на количество
- При успехе возвращается количество прочитанных байтов (0 указывает на конец файл).
- Не является ошибкой ситуация, если это число меньше, чем количество запрошенных байтов:
- мы были близки к концу файла;
- чтение выполняется с терминала (часто в этом случае идёт буферизация по строкам);
- чтение выполняется из сети;
- системный вызов был прерван сигналом.
write
#include ssize_t write(int fd, const void *buf, size_t count);Оптимальный размер буфера
Чтение и запись мелкими порциями неэффективна. Возникает вопрос, какой размер буфера стоит выставлять.
Рассмотрим пример из книги Advanced Programming in the UNIX Environment.
Программа выполняет копирование данных:
#include #include #define BUFFSIZE 4096 int main(void) { ssize_t n; char buf[BUFFSIZE]; while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) { if (write(STDOUT_FILENO, buf, n) != n) { perror("write error"); } } if (n 0) { perror("read error"); } return 0; }Тестирование производилось на файле размером 516,581,760. С двадцатью разными размерами буфера.
BUFFSIZE User CPU (s) System CPU (s) Clock time (s) Number of loops 1 20.03 117.50 138.73 516,581,760 2 9.69 58.76 68.60 258,290,880 4 4.60 36.47 41.27 129,145,440 8 2.47 15.44 18.38 64,572,720 16 1.07 7.93 9.38 32,286,360 32 0.56 4.51 8.82 16,143,180 64 0.34 2.72 8.66 8,071,590 128 0.34 1.84 8.69 4,035,795 256 0.15 1.30 8.69 2,017,898 512 0.09 0.95 8.63 1,008,949 1,024 0.02 0.78 8.58 504,475 2,048 0.04 0.66 8.68 252,238 4,096 0.03 0.58 8.62 126,119 8,192 0.00 0.54 8.52 63,060 16,384 0.01 0.56 8.69 31,530 32,768 0.00 0.56 8.51 15,765 65,536 0.01 0.56 9.12 7,883 131,072 0.00 0.58 9.08 3,942 262,144 0.00 0.60 8.70 1,971 524,288 0.01 0.58 8.58 986 Из-за того что на файловой системе ext4 использовались блоки размера 4096, дальнейшее увеличение буфера не даёт положительного эффекта.
Многие системы поддерживают read-ahead, то есть читают наперёд данные до того, как их запросило приложение, в надежде, что затем они понадобятся. Это видно на блоках размера от 32.
lseek
В каждом файле есть «текущее смещение (позиция)».
#include #include off_t lseek(int fd, off_t offset, int whence);
Третий параметр определяет тип операции:
Узнать текущую позицию:
off_t curpos; curpos = lseek(fd, 0, SEEK_CUR);
Не все файловые дескрипторы являются seekable. Именованный или анонимный канал, сокет нельзя перематывать.
#include #include int main() { if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1) { printf("cannot seek\n"); } else { printf("seek OK\n"); } return 0; }$ ./a.out cannot seek $ ./a.out < ./a.out seek OK
Если позиция устанавливается за границей файла, то файл расширяется до нужного размера, в нём образуется «дыра». Байты из неё читаются так, как будто там нули. В зависимости от файловой системы, эти нули могут физически писаться на диск, а могут не писаться (sparse file).
ftruncate
#include #include int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);Устанавливает размер файла равным заданному. Если файл был больше, он обрезается. Если был меньше, то дополняется нулями (опять же, если файловая система поддерживает, то будет создан sparse-файл и нули не будут фактически писаться на диск). Позиция не изменяется.
Принудительный сброс изменений на диск
Функции типа write копируют данные в буфер внутри ядра. Данные могут быть записаны физически на диск когда-нибудь (delayed write). Бывает иногда необходимо принудительно вызвать запись на диск, чтобы иметь гарантию, что данные не будут потеряны.
#include int fsync(int fd); int fdatasync(int fd); void sync(void);Функция sync просто добавляет в очередь на запись все модифицированные блоки и не ожидает конца их записи. Есть демон, который раз в 30 секунд вызывает sync. Также есть команда sync.
Функция fsync работает с одним файлом. Ждёт, пока данные не запишутся на диск. Может использоваться разными СУБД и подобными программами.
Функция fdatasync такая же по сути, но вызывает только запись данных, не атрибутов файла.
К сожалению, про fsync() в стандарте написано:
If _POSIX_SYNCHRONIZED_IO is not defined, the wording relies heavily on the conformance document to tell the user what can be expected from the system. It is explicitly intended that a null implementation is permitted.
Совместный доступ к файлам
- Каждый процесс представляется записью в таблице процессов — process table entry.
- У каждого процесса есть своя таблица открытых файловых дескрипторов. Можно считать, что это массив, индексом в котором выступает файловый дескриптор. Значением в таблице являются:
- флаги дескриптора, например close-on-exec,
- указатель на элемент таблицы файлов.
- Ядро ОС управляет таблицей для всех открытых файлов. Каждая запись — file table entry — содержит:
- флаги статуса (read, write, append, sync),
- текущее смещение (offset),
- указатель на структуру v-node.
- Структура под названием v-node содержит информацию о типе файла, указатели на функции, которые оперируют с этим файлом, а также i-node данного файла. В Linux данная сущность не называется v-node, там используется generic i-node, но это не столь важно.
Все эти структуры так или иначе используются при выполнении ввода-вывода.
- Как только завершился write, система проверяет позицию в файле и изменяет размер в i-node, если файл увеличился и должен быть расширен.
- Если файл открыт с флагом O_APPEND, соответствующий флаг выставляется в file table entry. Перед каждым выполнением write смещение выставляется равным размеру файла, который берётся из i-node.
- Если файл перематывается на конец при помощи lseek, его размер берётся из i-node.
- Функция lseek изменяет только смещение в file table entry. Операции I/O не выполняются.
fork
При вызовах fork у дочернего и родительского процесса появляется две независимые таблицы файловых дескрипторов. Однако file table entry при этом не копируется.
#include #include #include #include int main(void) int fd; fd = open("output", O_CREAT else { /* parent */ int status; wait(&status); write(fd, "world\n", 6); } }В результате в выходной файл будет записано
hello world
(смещение в файле хранится в file table entry и является общим у двух процессов).
Атомарные операции
Дозапись в файл
Старые программы могли делать это следующим образом:
if (lseek(fd, 0L, 2) 0) /* position to EOF */ err_sys("lseek error"); if (write(fd, buf, 100) != 100) /* and write */ err_sys("write error");
Есть проблема гонки между двумя системными вызовами.
На сегодняшний день для решения этой задачи следует открывать файл в режиме O_APPEND.
pread и pwrite
#include ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);Выполняют чтение и запись по заданному смещению.
Создание файла
Допустим, стоит задача создать файл, если он не существует, или вывести ошибку, если он уже существует.
Наивный подход такой:
if ((fd = open(path, O_WRONLY)) 0) { if (errno == ENOENT) { if ((fd = creat(path, mode)) 0) { err_sys("creat error"); } else { err_sys("open error"); } } }
Но тут возможна гонка.
Следует использовать комбинацию флагов O_CREAT и O_EXCL.
Операции с файловой системой
Операции rename, link, symlink, mkdir атомарны.
Если надо подменить файл:
- Записать данные во временный файл.
- Сделать fsync.
- Закрыть временный файл.
- Переименовать временный файл в требуемое имя.
Include unistd h что это
НАЗВАНИЕ
unistd - набор символических констант#include
ОПИСАНИЕ
Включаемый файл содержит символические константы и структуры, которые еще не были описаны в каких-либо других включаемых файлах.Символические константы для доступа к файлам:
#define R_OK 4 /* Маска доступности для чтения */ #define W_OK 2 /* Маска доступности для записи */ #define X_OK 1 /* Маска доступности для выполнения */ #define F_OK 0 /* Маска проверки существования файла */ #define F_ULOCK 0 /* Разблокировать прежде заблокированную область */ #define F_LOCK 1 /* Заблокировать область для монопольного использования */ #define F_TLOCK 2 /* Проверить и заблокировать область для монопольного использования */ #define F_TEST 3 /* Проверить, заблокирована ли область другими процессами */
Символические константы для позиционирования в файле:
#define SEEK_SET 0 /* Установить указатель файла равным смещению */ #define SEEK_CUR 1 /* Установить указатель файла равным текущему значению плюс смещение */ #define SEEK_END 2 /* Установить указатель файла равным EOF плюс смещение */
Маршрутные имена:
#define GF_PATH "/etc/group" /* Маршрутное имя файла групп */ #define PF_PATH "/etc/passwd" /* Маршрутное имя файла паролей */
unistd.h
Библиотека unistd.h
Доброго времени суток! У меня нету библиотеки unistd.h =( где ее можно скачать?Где посмотреть описание unistd.h
Доброго времени суток! Хотелось бы взглянуть на описание unistd.h, но не могу найти. Спасибо за.2473 / 1684 / 135
Регистрация: 14.05.2009
Сообщений: 7,162
просто так оно не заработает, можете не стараться
Регистрация: 03.10.2010
Сообщений: 190
Записей в блоге: 1
Сообщение от alex_x_x 
просто так оно не заработает, можете не стараться
а что надо сделать .
Почетный модератор
7391 / 2637 / 281
Регистрация: 29.07.2006
Сообщений: 13,696
Сообщение от Астик 
а что надо сделать .
в *nix компилять

21275 / 8292 / 637
Регистрация: 30.03.2009
Сообщений: 22,656
Записей в блоге: 30
Сообщение от Астик 
как добавить unistd.h в биб Visual studio . закидываю в папку с проектом читается не правильно (((
Это файл специфичный для unix-систем. Точно так же при компиляции под unix'ами можно как-то извернуться и добавить файл windows.h, только работать программа от этого всё равно не будет