Книга: UNIX — универсальная среда программирования

7.1 Ввод-вывод низкого уровня

7.1 Ввод-вывод низкого уровня

Низкий уровень ввода-вывода представляет собой непосредственный вход в операционную систему Ваши программы читают или пишут файлы порциями некоторого подходящего размера. Ядро буферизует данные порциями, которые подходят для периферийных устройств, и планирует операции для устройств так, чтобы их производительность была оптимальной для всех пользователей.

Дескрипторы файлов

Ввод и вывод обычно осуществляются посредством чтения или записи файлов, так как все периферийные устройства, и даже ваш терминал, представлены как файлы в файловой системе. Таким образом, единый интерфейс обеспечивает связь между программой и периферийными устройствами.

В самом общем случае, прежде чем читать или писать файл, необходимо сообщить системе о вашем намерении открыть файл. Если вы собираетесь писать в файл, может быть, его необходимо и создать. Система проверяет ваше право сделать это (существует ли файл и есть ли у вас разрешение на доступ к нему?), и при положительном результате проверки возвращает неотрицательное целое, называемое дескриптором файла. Всякий раз, когда нужно выполнить ввод-вывод через файл, для его идентификации вместо имени используется дескриптор файла. Вся информация об открытом файле поддерживается системой. Ваша программа ссылается на файл только через дескриптор файла. Указатель на FILE, как отмечалось в гл. 6, является ссылкой на структуру, которая наряду с прочим содержит дескриптор файла: макрокоманда fileno(fp), определенная в <stdio.h>, возвращает дескриптор файла.

Для обеспечения удобства ввода и вывода на терминал предусмотрены специальные меры. Когда программа запускается из shell, она получает три открытых файла с дескрипторами 0, 1 и 2 стандартный входной поток, стандартный выходной поток и стандартный файл диагностики. Всем им по умолчанию поставлен в соответствие терминал, поэтому если программа читает только через дескриптор файла 0 и пишет через дескрипторы файлов 1 и 2, она может выполнять ввод-вывод без открывания файлов. Если же программа открывает любые другие файлы, они будут иметь дескрипторы 3, 4 и т.д.

При переключении ввода-вывода на файлы или программные каналы (к ним или от них) shell изменяет назначение терминала по умолчанию для дескрипторов файлов 0 и 1. Обычно дескриптор файла 2 закрепляется за терминалом, так что сообщения об ошибках могут поступать на него. Использование символики shell, такой, как 2>filename и 2>&1, вызовет переназначение файла, присвоенного по умолчанию, но при этом присвоение файлов меняется в shell, а не программой. (Программа сама может переназначить их впоследствии, если потребуется, что, правда, бывает редко.)

Файловый ввод-вывод: read и write

Весь ввод и вывод обеспечиваются двумя системными вызовами, read и write, которые доступны в Си с помощью функций с теми же именами. Для обеих первый аргумент это дескриптор файла, второй массив байтов, который служит источником данных или назначением, а третий число байтов, которые следует передать.

int fd, n, nread, nwritten;
char buf[SIZE];
nread = read(fd, buf, n);
nwritten = write(fd, buf, n);

Каждый вызов возвращает число переданных байтов. При чтении возвращенное число может быть меньше, чем запрошенное, поскольку для чтения оставлено менее n байт. (Когда файлу поставлен в соответствие терминал, read обычно читает до следующей строки, что составляет меньшую часть запрошенного.) Возвращаемое значение 0 подразумевает конец файла, а значение -1 обозначает некоторую ошибку. При записи возвращаемое значение есть число действительно записанных байтов; если оно не равно числу байтов, которое предполагается записать, возникает ошибка. Несмотря на то, что число байтов, которые следует читать или писать, не ограничено, наиболее часто используются два значения: 1, что соответствует одному символу за одно обращение ("не буферизовано"), и размер блока на диске, как правило,- 512 или 1024 байта (такое значение имеет BUFSIZ в <stdio.h>). Для иллюстрации изложенного здесь приведена программа копирования входного потока в выходной. Так как входной и выходной потоки могут переключаться на любой файл или устройство, она действительно скопирует что-нибудь куда-либо: это "скелетная" реализация cat.

/* cat: minimal version */
#define SIZE 512 /* arbitrary */
main() {
 char buf[SIZE];
 int n;
 while ((n = read(0, buf, sizeof buf)) > 0)
  write(1, buf, n);
 exit(0);
}

Если размер файла не кратен числу SIZE, некоторый вызов read вернет меньшее число байтов, которые должны быть записаны с помощью write; следующий затем вызов read вернет нуль.

Чтение и запись порциями, подходящими для диска, будут наиболее эффективными, но даже ввод-вывод по одному символу за раз осуществим для умеренных объемов буферизуются ядром. Дороже всего обходятся обращения к системе. Программа ed, например, использует однобайтовый способ, чтобы читать стандартный входной поток. Мы хронометрировали работу данной версии cat для файла в 54 000 байт при шести значениях SIZE:

Время (пользователь+система, в сек.)

Размер PDP-11/40 VAX-11/750
1 271.0 188.8
10 29.9 19.3
100 3.8 2.6
512 1.3 1.0
1024 1.2 0.6
5120 1.0 0.6

Размер блока на диске для системы на PDP-11 составляет 512 байт и 1024 байта — для VAX.

Доступ нескольких процессоров к одному и тому же файлу в одно и то же время является совершенно законным: в самом деле, один процесс может писать, в то время как другой читает. Это обескураживает, если не входит в ваши планы, но иногда оказывается полезным. Даже несмотря на то, что при одном обращении к функции read возвращается 0, который сигнализирует о конце файла, следующий вызов read обнаружит наличие некоторого количества байтов, если еще данные пишутся в файл. Это соображение лежит в основе программы readslow, которая продолжает читать свой входной поток вне зависимости от того, достигла она конца файла или нет. Программа readslow удобна для наблюдения за работой программы.

$ slowprog >temp &
5213
идентификатор процесса

$ readslow <temp | grep something

Иными словами, медленная программа выполняет вывод в файл; readslow, возможно, совместно с некоторыми другими программами "наблюдает", как накапливаются данные.

По составу readslow идентична cat, за исключением того, что она зацикливается, а не завершается, когда встречает конец входного потока. Программа readslow должна использовать ввод-вывод низкого уровня, так как стандартная библиотечная функция продолжает выдавать EOF после первого конца файла.

/* readslow: keep reading, waiting for more */
#define SIZE 512 /* arbitrary */
main() {
 char buf[SIZE];
 int n;
 for (;;) {
  while ((n = read(0, buf, sizeof buf)) > 0)
   write(1, buf, n);
  sleep(10);
 }
}

Функция sleep заставляет программу остановиться на определенное число секунд (см. справочное руководство по sleep(3)). Мы не хотим, чтобы программа долго занималась поиском дополнительных данных, так как на это расходуется время центрального процессора. Таким образом, наша версия readslow копирует свой входной поток до конца файла, "спит" какое-то время, затем снова возобновляет работу. Если пока она была "в паузе", пришли еще данные, они будут прочитаны следующим read.

Упражнение 7.1

Добавьте readslow аргумент n, так что установленное по умолчанию время паузы может быть изменено на n секунд. Некоторые системы обеспечивают флаг -f ("навсегда") для tail, которая объединяет функции tail и readslow. Прокомментируйте этот вариант.

Упражнение 7.2

Что происходит с readslow, если читаемый файл обрывается? Как бы вы исправили ситуацию? Подсказка: читайте о fstat в разд. 7.3.

Создание файла: open, creat, close, unlink

Все стандартные файлы, кроме установленных по умолчанию, — входной, выходной и файл диагностики вы должны явно открыть для чтения или записи. Это можно сделать с помощью двух системных вызовов — открыть и создать[14].

Функция open весьма похожа на fopen из предыдущей главы, за исключением того, что вместо указателя файла она возвращает дескриптор файла, имеющий тип int.

char *name;
int fd, rwmode;
fd = open(name, rwmode);

Как и для fopen, аргумент name есть символьная строка, содержащая имя файла. Аргумент вида доступа, однако, другой: rwmode равен 0 для чтения, 1 — для записи, 2 — в том случае, когда нужно открыть файл для чтения и записи. При вызове open возвращается -1, если возникает какая-либо ошибка; иначе возвращается корректный дескриптор файла.

Попытка открыть несуществующий файл является ошибкой. Системный вызов creat позволяет создать новые файлы или переписать старые.

int perms;
fd = creat(name, perms);

Вызов creat возвращает дескриптор файла, если можно создать файл name, и -1 в противном случае. Если файл не существует, creat создает его с правами доступа, определяемыми аргументом perms. Существующий файл creat сокращает до нулевой длины, т.е. применение creat к уже существующему файлу не является ошибкой (права доступа при этом не изменяются). Безотносительно к правам доступа файл, к которому было обращение creat, открыт для записи.

Как известно из второй главы, с файлом связаны девять битов информации о защите, контролирующих разрешение на чтение, запись или выполнение, так что число из трех восьмеричных цифр удобно для спецификации этой информации. Например, 0755 дает разрешение владельцу файла читать, писать и выполнять его, а чтение и выполнение файла доступно любому пользователю. Не забывайте о первом нуле, который определяет восьмеричные числа в языке Си.

Иллюстрацией изложенного может служить упрощенная версия cp. Ее главный недостаток состоит в том, что она копирует только один файл и не разрешает использовать в качестве второго аргумента каталог. Кроме того, наша версия не сохраняет права доступа файла-источника; в дальнейшем мы покажем, как это исправить.

/* cp: minimal version */
#include <stdio.h>
#define PERMS 0644 /* RW for owner, R for group, others */
char *progname;
main(argc, argv) /* cp: copy f1 to f2 */
 int argc;
 char *argv[];
{
 int f1, f2, n;
 char buf[BUFSIZ];
 progname = argv[0];
 if (argc != 3)
  error("Usage: %s from to", progname);
 if ((f1 = open(argv[1], 0)) == -1)
  error("can't open %s", argv[1]);
 if ((f2 = creat(argv[2], PERMS)) == -1)
  error("can't create %s", argv[2]);
 while ((n = read(f1, buf, BUFSIZ)) > 0)
  if (write(f2, buf, n) != n)
 error("write error", (char*)0);
 exit(0);
}
error
мы обсудим ниже.

Число файлов, которые одновременно могут быть открыты программой, ограничено (обычно порядка 20; см. NOFILE в <SYS/param.h>). Поэтому любая программа, которой предстоит обрабатывать много файлов, должна быть готова неоднократно использовать одни и те же дескрипторы файлов. Системный вызов close разрывает связь между именем и дескриптором файла, освобождая дескриптор для использования с некоторым другим файлом. Завершение программы посредством exit и возврат из основной программы закрывают все открытые файлы. Вызов системы unlink удаляет файл из файловой системы.

Обработка ошибок: errno

Обсуждаемые здесь системные вызовы, а по сути все системные вызовы, могут вызывать ошибки. Обычно они сигнализируют об ошибке, возвращая значение -1. Иногда полезно знать, какая именно ошибка произошла, поэтому системные вызовы, когда это приемлемо, оставляют номер ошибки во внешней целой переменной, называемой errno. (Значение различных номеров ошибок объясняется во введении к разд. 2 справочного руководства по UNIX.) С помощью errno ваша программа может определить, например, чем вызвана неудача при открытии файла — тем, что он не существует, или тем, что у вас нет разрешения на его чтение. Кроме того, есть массив символьных строк sys_errlist, индексируемый errno, который переводит число в строку, передающую смысл ошибки. Наша версия error использует эти структуры данных:

error(s1, s2) /* print error message and die */
 char *s1, *s2;
{
 extern int errno, sys_nerr;
 extern char *sys_errlist[], *progname;
 if (progname)
  fprintf(stderr, "%s: ", progname);
 fprintf(stderr, s1, s2);
 if (errno > 0 && errno < sys_nerr)
  fprintf (stderr, " (%s)", sys_errlist[errno]);
 fprintf(stderr, "n");
 exit(1);
}
Errno
первоначально равна нулю и всегда должна быть меньше, чем sys_herr. Она не становится нулевой вновь при нормальной работе, поэтому вы должны обнулять ее после каждой ошибки, если ваша программа будет продолжать выполняться. Сообщения об ошибках в нашей версии cp появляются следующим образом:

$ cp foo bar
cp: can't open foo      
(Нет такого файла или каталога)

$ date >foo; chmod 0 foo Создать нечитаемый файл

$ cp too bar
cp: can't open foo      
(В разрешении отказано)

$

Произвольный доступ: lseek

Файл ввода-вывода обычно последовательный: каждый read или write занимает место в файле непосредственно после использованного при предыдущем вызове. Однако при необходимости файл может быть прочитан или записан в произвольном порядке. Системный вызов lseek позволяет перемещаться по файлу, не осуществляя ни чтения, ни записи:

int fd, origin;
long offset, pos, lseek();
pos = lseek(fd, offset, origin);

Текущая позиция в файле с дескриптором fd перемещается к позиции offset, которая отсчитывается относительно места, определяемого origin. Последующие процессы чтения или записи будут начинаться с этой позиции. Origin может иметь значения 0, 1, 2, задавая тем самым начало отсчета значения offset — от начала, от текущей позиции или от конца файла соответственно.

Возвращаемое значение есть новая абсолютная позиция или -1 при ошибке. Например, при добавлении информации в файл нужно дойти до его конца, а затем выполнить запись:

lseek(fd, 0L, 2);

Чтобы вернуться обратно к началу ("перемотать"), необходимо вызвать

lseek(fd, 0L, 0);

Для определения текущей позиции следует выполнить

pos = lseek(fd, 0L, 1);

Обратите внимание на аргумент 0L: смещение есть длинное целое. ('l' в lseek означает 'long' — длинный, чтобы отличить его от системного вызова seek в шестой версии, где используются короткие целые.)

С помощью lseek можно обращаться с файлами как с большими массивами, однако при этом время доступа к ним возрастает. Например, следующая функция читает любое число байтов из любого места в файле:

get(fd, pos, buf, n) /* read n bytes from position pos */
 int fd, n;
 long pos;
 char *buf;
{
 if (lseek(fd, pos, 0) == -1) /* get to pos */
  return -1;
 return read(fd, buf, n);
}

Упражнение 7.3

Модифицируйте readslow так, чтобы обрабатывать имя файла в качестве аргумента, если оно присутствует. Добавьте :

$ readslow -е

заставляет readslow искать конец входного потока, прежде чем начать чтение. Каковы функции lseek при работе с программным каналом?

Упражнение 7.4

Перепишите efopen из гл. 6, чтобы вызвать error.

Оглавление книги


Генерация: 0.161. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз