Б. Керриган, Д. Ритчи Язык программирования C. Б. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003
Скачать 31.48 Mb.
|
Глава 8 Интерфейс с системой UNIX Свои услуги операционная система UNIX предлагает в виде набора системных вызовов, которые фактически являются ее внутренними функ- циями и к которым можно обращаться из программ пользователя. В на- стоящей главе описано, как в Си-программах можно применять некото- рые наиболее важные вызовы. Если вы работаете в системе UNIX, то эти сведения будут вам полезны непосредственно и позволят повысить эф- фективность работы или получить доступ к тем возможностям, которых нет в библиотеке. Даже если вы используете Си в другой операционной системе, изучение рассмотренных здесь примеров все равно приблизит вас к пониманию программирования на Си; аналогичные программы (от- личающиеся лишь деталями) вы встретите практически в любой опера- ционной системе. Так как библиотека Си-программ, утвержденная в ка- честве стандарта ANSI, в основном отражает возможности системы UNIX, предлагаемые программы помогут вам лучше понять и библиотеку. Глава состоит из трех основных частей, описывающих: ввод-вывод, файловую систему и организацию управления памятью. В первых двух частях предполагается некоторое знакомство читателя с внешними ха- рактеристиками системы UNIX. . В главе 7 мы рассматривали единый для всех операционных систем ин- терфейс ввода-вывода. В любой конкретной системе программы стандарт- ной библиотеки пишутся с использованием средств именно этой кон- кретной системы. В следующих нескольких параграфах мы опишем вы- зовы системы UNIX по вводу-выводу и покажем, с их помощью можно реализовать некоторые разделы стандартной библиотеки. 8.1. Дескрипторы файлов В системе UNIX любые операции ввода-вывода выполняются посред- ством чтения и записи файлов, поскольку все внешние устройства, 8.2. Нижний уровень ввода-вывода (read и write) 217 чая клавиатуру и экран, рассматриваются как файловой систе- мы. Это значит, что все связи между программой и внешними устройства- ми осуществляются в рамках единого однородного интерфейса. В самом общем случае, прежде чем читать или вы должны про- информировать систему о действиях, которые вы намереваетесь выпол- нять в отношении файла; эта процедура называется файла. Если вы собираетесь писать в файл, то, возможно, его потребуется создать заново или очистить от хранимой информации. Система проверяет ваши права на эти действия (файл существует? вы имеете к нему доступ?) и, если все в порядке, возвращает программе небольшое неотрицательное целое, называемое дескриптором файла. Всякий раз, когда осуществля- ется ввод-вывод, идентификация файла выполняется по его дескрипто- ру, а не по имени. (Дескриптор файла аналогичен файловому указателю, используемому в стандартной библиотеке, или хэндлу (handle) в MS- DOS.) Вся информация об открытом файле хранится и обрабатывается операционной системой; программа пользователя обращается к файлу только через его дескриптор. Ввод с клавиатуры и вывод на экран применяются настолько что для удобства работы ними предусмотрены специальные соглашения. При программы командный интерпретатор (shell) открывает три фай- ла с дескрипторами и 2, которые называются соответственно стандарт- ным вводом, стандартным выводом и стандартным файлом ошибок. Если программа читает из файла 0, а пишет в файлы 1 и 2 (здесь цифры - де- скрипторы файлов), то она может осуществлять ввод и вывод, не забо- тясь об их открытии. Пользователь программы имеет возможность перенаправить ввод-вы- вод в файл или из файла с помощью значков < и >, как, например, в prog В этом случае командный интерпретатор заменит стандартные установ- ки дескрипторов 0 и 1 на именованные файлы. Обычно дескриптор фай- ла 2 остается подсоединенным к экрану, чтобы на него шли сообщения об ошибках. Сказанное верно и для ввода-вывода, связанного в конвейер. Во всех случаях замену файла осуществляет командный интерпретатор, а не программа. Программа, если она ссылается на файл 0 (в случае вво- да) и файлы 1 и 2 (в случае вывода), не знает, ни откуда приходит ее ввод, ни куда отправляется ее вывод. 8.2. Нижний уровень ввода-вывода и write) Ввод-вывод основан на системных вызовах read и write, к которым Си-программа обращается с помощью функций с именами read и write. 218 __ Глава 8. Интерфейс с системой UNIX Для обеих первым аргументом является дескриптор файла. Во втором ар- гументе массив символов вашей программы, куда посыла- ются или откуда берутся данные. Третий аргумент — это количество пе- ресылаемых байтов. n_read = read(int fd, char *buf, int n); int = fd, char *buf, int n); Обе функции возвращают число переданных байтов. При чтении чество прочитанных байтов может оказаться меньше числа, указанного в третьем аргументе. Нуль означает конец файла, а -1 сигнализирует о какой-то ошибке. При записи функция возвращает количество записан- ных байтов, и если это число не совпадает с следует считать, что запись не произошла. За один вызов можно прочитать или записать любое число байтов. Обычно это число равно или 1, что означает посимвольную передачу "без буферизации", или чему-нибудь вроде или 4096, соответствующих размеру физического блока внешнего устройства. Эффективнее обмени- ваться числом байтов, поскольку при этом требуется меньше системных вызовов. Используя полученные сведения, мы можем напи- сать простую программу, копирующую свой ввод на свой вывод и эквива- лентную программе копирования файла, описанной в главе С помощью этой программы можно копировать откуда угодно и куда угодно, посколь- ку всегда существует возможность перенаправить ввод-вывод на любой файл или устройство. , /* копирование ввода на вывод */ char int n; while = read(0, BUFSIZ)) > 0) write(i, n); return 0; Прототипы функций, обеспечивающие системные вызовы, мы собра- ли в файле h, что позволяет нам включать его в программы этой главы. Однако имя данного файла не зафиксировано стандартом. Параметр BUFSIZ также определен в 8.2. Нижний уровень ввода-вывода (read и write) 219 то какая-то операция чтения вернет значение меньшее, чем BUFSIZ, а сле- дующее обращение к read даст в качестве результата нуль. Полезно рассмотреть, как используются read и write при написании программ более высокого уровня - таких как r, p u t c h a и. т. д. Вот, к примеру, версия программы getcha г, которая осуществляет небуферизо- ванный ввод, читая по одному символу из стандартного входного потока. /* getchar: небуферизованный ввод одного символа */ int >: char с; return (read(0, &c, 1) == 1) ? (unsigned char) с : EOF; } Переменная с должна быть типа поскольку read требует указателя на Приведение с к unsigned перед как вернуть ее в качестве результата, исключает какие-либо проблемы, связанные с распростране- нием знака. Вторая версия осуществляет ввод большими кусками, но при каждом обращении выдает только один символ. "syscalls. h" * /* getchar: простая версия с буферизацией */ int getchar(void) { static char static char = buf; static int n = 0; (n == 0) { /* буфер пуст '*/ n = read(0, buf, sizeof buf); bufp = } return >= 0) ? (unsigned char) *bufp++ : EOF; } Если приведенные здесь версии функции getcha r компилируются с вклю- чением заголовочного файла и в этом заголовочном файле getchar определена как макрос, то нужно задать строку с именем getchar. 220 Глава 8. Интерфейс с системой UNIX 8.3. Системные вызовы open, creat, close, unlinc В отличие от стандартных файлов ввода, вывода и ошибок, которые открыты по умолчанию, остальные файлы нужно открывать явно. Для этого есть два системных вызова: open и с Функция open почти совпадает с open, рассмотренной в главе 7. Раз- ница между ними в том, что первая возвращает не файловый указатель, а дескриптор файла типа int. При любой ошибке open возвращает flinclude int int flags, int perms); fd = flags, Как и в f open, аргумент name - это строка, содержащая имя файла. Второй аргумент, имеет тип int и специфицирует, каким образом должен быть открыт файл. Его основными значениями являются: 0_RDONLY - открыть только на чтение; - открыть только на запись; - открыть и на чтение, и на В System V UNIX эти константы определены в а в версиях Berkley (BCD) - в h>. открыть существующий файл на чтение, можно написать fd = 0_RDONLY, 0); Далее везде, где мы пользуемся функцией open, ее аргумент perms равен нулю. Попытка открыть несуществующий файл является ошибкой. нового файла или перезапись старого обеспечивается системным вызо- вом Например int creat(char int perms); fd = Функция reat возвращает дескриптор файла, если файл создан, и если по каким-либо причинам файл создать не удалось. Если файл уже суще- с reat "обрежет" его до нулевой длины, что равносильно выбрасы- ванию предыдущего содержимого данного файла; создание уже существу- ющего файла не является ошибкой. Если строится действительно новый файл, то его создаст с пра- вами доступа, специфицированными в аргументе ре В системе UNIX с каждым файлом ассоциированы девять битов, содержащие информа- 8.3. Системные вызовы open, creat, close, а правах пользоваться этим файлом для чтения, записи и исполне- ния лицам трех категорий: собственнику файла, определенной им группе лиц и всем остальным. Таким образом, права доступа удобно ровать с помощью трех восьмеричных цифр. Например, 0755 специфици- рует чтение, запись и право исполнения собственнику файла, а также чте- ние и право исполнения группе и всем остальным. Для иллюстрации приведем упрощенную версию программы ср систе- мы UNIX, которая копирует один файл в другой. В нашей версии копи- руется только один файл, не позволяется во втором аргументе указывать директорий (каталог), и права доступа не а задаются кон- стантой. «define PERMS 0666 /* RW для собственника, группы и остальных */ void error(char /* ср: копирование f1 в */ argc, char { int f1, f2, n; char if (argc 3) ср откуда if = open(argv[1], == -1) error ("ср: не могу открыть файл if = creat(argv[2], == -1) error ("ср: не могу создать файл %s, режим argv[2], PERMS); while = read(f1, buf, BUFSIZ)) > 0) if buf, n) n) error ("ср: ошибка при записи в файл return 0; Данная программа создает файл вывода с фиксированными правами до- ступа, определяемыми кодом 0666. С помощью системного вызова stat, который будет описан в параграфе 8.6, мы можем определить режим ис- пользования существующего файла и задать тот же режим для копии. Заметим, что функция e r r o r , вызываемая с различным числом аргу- ментов, во многом похожа на р ri Реализация ro r иллюстрирует, как 222 Глава 8. Интерфейс с системой UNIX пользоваться другими программами семейства p r i n t f . Библиотечная функция аналогична с той лишь оговоркой, что перемен- ная часть списка аргументов заменена в ней одним аргументом, который инициализируется макросом va_sta Подобным же образом соотносят- ся функции printf с printf и vsprintf с /* error: печатает сообщение об ошибке и умирает */ void error(char { va_list "ошибка: fmt, args); fprintf (stderr, exit(1); } На количество одновременно открытых в программе файлов имеется ограничение (обычно их число колеблется около 20). Поэтому любая про- грамма, которая намеревается работать с большим количеством файлов, должна быть готова повторно использовать их дескрипторы. Функция close(int fd) разрывает связь между файловым дескриптором и откры- тым файлом и освобождает дескриптор для его применения с другим фай- лом. Она аналогична библиотечной функции f close с тем лишь различи- ем, что никакой очистки буфера не делает. Завершение программы с по- мощью exit или retu rn в главной программе закрывает все открытые фай- лы. Функция удаляет имя файла из файловой системы. Она соответствует функции remove стандартной библиотеки. Упражнение 8.1. Перепишите программу cat из главы 7, используя функции read, write, open и close. Замените ими соответствующие функ- ции стандартной библиотеки. Поэкспериментируйте, чтобы сравнить быстродействие двух версий. 8.4. Произвольный доступ Ввод-вывод обычно бывает последовательным, т. е. каждая новая опе- рация имеет дело с позицией следующей за той, что 8.4. Произвольный доступ 223 была в операции (чтения-записи). При однако, файл можно читать или производить запись в него в произвольном по- рядке. Системный вызов Iseek предоставляет способ передвигаться по файлу, не читая и не записывая данные. функция long lseek(int long offset, int origin); в файле с дескриптором f d устанавливает текущую позицию, смещая ее на величину относительно места, задаваемого значением origin. Зна- чения параметра origin или 2 означают, что на величину отсту- пают соответственно от начала, от текущей позиции или от конца файла. Например, если требуется добавить что-либо в файл (когда в командном интерпретаторе shell системы UNIX ввод перенаправлен оператором в файл или когда в f open задан аргумент то прежде чем что-либо за- писывать, необходимо найти конец файла с помощью вызова функции lseek(fd, OL, 2); Чтобы вернуться назад, в начало файла, надо выполнить lseek(fd, OL, ,0); Следует обратить внимание на аргумент OL: вместо OL можно было бы написать 0 или, если функция Iseek должным образом объявлена, просто 0. Благодаря Iseek с файлами можно работать так, как будто это большие массивы, правда, с замедленным доступом. Например, следующая функ- ция читает любое число байтов из любого места файла. Она возвращает число прочитанных байтов или в случае ошибки. • /* get: читает п байт из позиции */ int fd, long pos, char *buf, int n) { if (lseek(fd, pos, 0) >= 0) /* установка позиции */ return read(fd, buf, n); else return } Возвращаемое функцией Iseek значение имеет тип long и является новой позицией в файле или, в случае ошибки, равно Функция f seek из стан- дартной библиотеки аналогична Iseek; от последней она отличается тем, что в случае ошибки возвращает некоторое ненулевое значение, а ее пер- вый аргумент имеет тип 224 Глава 8. Интерфейс с системой UNIX 8.5. Пример. Реализация функций и getc Теперь на примере функций и getc из стандартной библиотеки покажем, как описанные выше части согласуются друг с другом. Напомним, что файлы в стандартной библиотеке описываются файло- выми указателями, а не дескрипторами. Указатель файла - это указатель на структуру, содержащую информацию о файле: указатель на буфер, по- зволяющий читать файл большими кусками; число незанятых байтов бу- фера; указатель на следующую позицию в буфере; дескриптор файла; флаж- ки, описывающие режим (чтение/запись), ошибочные состояния и т. д. Структура данных, описывающая содержится в h>, кото- рый необходимо включать (с помощью в любой исходный файл, если в том осуществляется стандартный ввод-вывод. Этот же заголовоч- ный файл включен и в исходные тексты библиотеки ввода-вывода. В следующем фрагменте, типичном для файла h>, имена, исполь- зуемые только в библиотечных функциях, начинаются с подчеркивания. Это сделано для того, чтобы они случайно не совпали с именами, фигу- рирующими в программе пользователя. Такое соглашение соблюдается во всех программах стандартной библиотеки. ; NULL О EOF (-1) BUFSIZ 1024 20 /* max число одновременно открытых файлов */ typedef struct { int cnt; /* количество оставшихся символов */ char *ptr; /* позиция следующего символа */ char /* адрес буфера */ int flag; /* режим доступа */ int fd; /* дескриптор файла */ } FILE; extern FILE stdin «define stdout stderr _flags { _READ /* файл открыт на чтение */ = 02, /* файл открыт на запись */ _UNBUF = 04, /* файл не буферизуется */ 8.5. Пример. Реализация функций и getc _ 225 _EOF = 010, /* в данном файле встретился EOF */ _ERR = 020 /* в данном файле встретилась ошибка */ *); int _flushbuf(int, FILE *); feof(p) & _EOF) != 0) & _ERR) != 0) fileno(p) getc(p) >= 0 \ ? (unsigned char) *(p)->ptr++ : «define >= 0 \ ? *(p)->ptr++ = (x) : getchar() getc(stdin) «define putchar(x) putc((x), stdout) Макрос getc обычно уменьшает счетчик числа символов, находящихся в буфере, и возвращает символ, после чего приращивает указатель на еди- ницу. (Напомним, что длинные с помощью обратной наклонной черты можно продолжить на следующих строках.) Когда значение счет- чика становится отрицательным, getc вызывает _f чтобы снова за- полнить буфер, инициализировать содержимое структуры и выдать сим- вол. Типы возвращаемых символов приводятся к unsigned; это гаранти- рует, что все они будут положительными. Хотя в деталях ввод-вывод здесь не рассматривается, мы все же приве- ли полное определение putc. Сделано это, чтобы показать, что она дей- ствует во многом так же, как и getc, вызывая функцию когда буфер полон. В тексте имеются макросы, позволяющие получать доступ к флажкам ошибки и конца файла, а также к его дескриптору. Теперь можно написать функцию fopen. Большая часть инструкций относится к открытию файла, к соответствующему его позициони- рованию и к установке флажковых битов, предназначенных для индика- ции текущего состояния. Сама не отводит места для буфера; это делает _f i l l b u f при первом чтении файла. PERMS 0666 /* для собственника, группы и проч. */ /* fopen: открывает файл, файловый указатель */ FILE char 1116 226 Глава 8. Интерфейс с системой UNIX int fd; FILE *fp; if != ' && != V && != return NULL; for (fp = _iob; fp < + OPEN_MAX; fp++) if & (_READ ! == 0) break; /* найдена свободная if (fp >= _iob + /* нет свободной позиции */ return NULL; if == fd = else if == { if = == -1) fd = PERMS); lseek(fd, 2); } else fd = 0_RDONLY, 0); if (fd == -1) /* невозможен доступ по имени name */ return NULL; = fd; fp->cnt = 0; fp->base = NULL; fp->flag = == ? _READ : return fp; } Приведенная здесь версия f open реализует не все режимы доступа, огово- ренные стандартом; но, мы думаем, их реализация в полном объеме не намного увеличит длину программы. Наша f open не распознает буквы сигнализирующей о бинарном вводе-выводе (поскольку в системах UNIX это не имеет смысла), и знака +, указывающего на возможность одновре- менно читать и писать. Для любого файла в момент первого обращения к нему с помощью макро- вызова getc счетчик cnt равен нулю. Следствием этого будет вызов _f Если выяснится, что файл на чтение не то функция _f illbuf немед- ленно возвратит EOF. В противном случае она попытается запросить память для буфера (если чтение должно быть с После получения области памяти для буфера _f i l l b u f обращается к read, чтобы его наполнить, устанавливает счетчик и указатели и воз- вращает первый символ из буфера. В следующих обращениях обнаружит, что память для буфера уже выделена. 8.5. Пример. Реализация функций и getc 227 /* запрос памяти и заполнение буфера */ int _fillbuf(FILE *fp) { int _READ) return EOF; bufsize = (fp->flag & ? 1 : BUFSIZ; if (fp->base == NULL) /* буфера еще нет */ if = (char *) == NULL) return EOF; /* нельзя получить буфер */ = fp->base; fp->cnt = read(fp->fd, fp->ptr, bufsize); if < 0) { if (fp->cnt == -1) _EOF; else _ERR; fp->cnt = 0; return EOF; } return (unsigned char) *fp->ptr++; Единственное, что осталось невыясненным, - это каким образом орга- низовать начало счета. Массив _iob следует определить и инициализиро- вать так, чтобы перед тем как программа начнет работать, в нем уже была информация о файлах stdin, и stde rr. FILE = { /* stdin, stdout, stderr: */ { 0, (char *) 0, (char *) 0, _READ, 0 }, { 0, (char *) 0, (char *) 0, 1 }, { 0, (char 0, (char *) 0, i 2 } }; Инициализация как части структуры показывает, что stdin открыт на чтение, stdout - на запись, - на запись без буферизации. Упражнение 8.2. Перепишите функции и работая с флаж- ками как с полями, а не с помощью явных побитовых операций. Сравните размеры и скорости двух вариантов программ. 228 Глава 8. Интерфейс с системой UNIX Упражнение 8.3. Разработайте и напишите функции f flush и fclose. Упражнение 8.4. Функция стандартной библиотеки int fseek(FILE *fp, long offset, int origin) идентична функции с теми, отличиями, что f p - это файловый указатель, а не дескриптор, и возвращает она значение означающее состояние файла, а не позицию в нем. Напишите свою версию f seek. Обеспечьте, чтобы работа вашей f seek по буферизации была согласована с буферизацией, используемой другими функциями библиотеки. 8.6. Пример. Печать каталогов При разного рода взаимодействиях с файловой системой иногда тре- буется получить информацию о файле, а не его содержимое. Та- кая потребность возникает, например, в программе печати каталога фай- лов, работающей аналогично команде системы UNIX. Она печатает имена файлов каталога и по желанию пользователя другую дополнитель- ную информацию (размеры, права доступа и т. д.). Аналогичной коман- дой в MS-DOS является di г. Так как в системе UNIX каталог - это тоже файл, функции чтобы добраться до имен файлов, нужно только его прочитать. Но чтобы полу- чить другую информацию о файле (например узнать его размер), необхо- димо выполнить системный вызов. В других системах (в MS-DOS, на- пример) системным вызовом приходится пользоваться даже для получе- ния доступа к именам файлов. Наша цель - обеспечить доступ к инфор- мации по возможности системно-независимым способом несмотря то, что реализация может быть существенно системно-зависима. Проиллюстрируем сказанное написанием программы f size. Функция f size — частный случай программы она печатает размеры всех файлов, перечисленных в командной строке. Если какой-либо из файлов сам яв- ляется каталогом, то, чтобы получить информацию о нем, fsize обраща- ется сама к себе. Если аргументов в командной строке нет, то обрабатыва- ется текущий каталог. Для начала вспомним структуру файловой системы в Каталог - это файл, содержащий список имен файлов и некоторую информацию о том, где они расположены. расположения" - это индекс, обеспе- чивающий доступ в другую таблицу, называемую "списком узлов Для каждого файла имеется свой inode, где собрана вся информация о фай- 8.6. Пример. Печать каталогов 229 ле, за исключением его имени. Каждый элемент каталога состоит из двух частей: из имени файла и номера узла mode. К сожалению, формат и точное содержимое каталога не одинаковы в разных версиях Поэтому, чтобы переносимую компоненту от- делить от непереносимой, разобьем нашу задачу на две. Внешний уро- вень определяет структуру, названную Oi rent, и три подпрограммы opendi г, и г; в результате обеспечивается системно-независимый доступ к имени и номеру узла inode каждого элемента каталога. Мы бу- дем писать программу fsize, рассчитывая на такой интерфейс, а затем покажем, как реализовать указанные функции для систем, использующих ту же структуру каталога, что и Version 7 и System V UNIX. Другие вари- анты оставим для упражнений. Структура Di rent содержит номер узла inode и имя. Максимальная дли- на имени файла равна - это значение системно-зависимо. Функ- ция opendi г возвращает указатель на структуру, названную DIR (по ана- логии с FILE), которая используется функциями readdir и closedir. Эта информация сосредоточена в заголовочном файле di h. tfdefine 14 /* максимальная длина имени файла; */ системно-зависимая величина */ typedef struct { /* универс. структура элемента */ long ino; /* номер inode */ char /* имя + завершающий */ } typedef struct { /* минимальный DIR: без буферизации и т.д. */ int fd; /* файловый дескриптор каталога */ Dirent d; /* элемент каталога */ } DIR; DIR *opendir(char Dirent *readdir(DIR *dfd); void Системный вызов stat получает имя файла и возвращает полную о нем информацию, содержащуюся в узле inode, или -1 в случае ошибки. Так, char struct stat stbuf; int stat(char *, struct stat *); &stbuf); заполняет структуру stbuf информацией из узла inode о файле с именем |