4. Работа с файлами.
Файлы представляют собой области памяти на внешнем носителе (как правило магнит-
ном диске), предназначенные для:
- хранения данных, превосходящих по объему память компьютера (меньше, разумеется,
тоже можно);
- долговременного хранения информации (она сохраняется при выключении машины).
В UNIX и в MS DOS файлы не имеют предопределенной структуры и представляют собой
просто линейные массивы байт. Если вы хотите задать некоторую структуру хранимой
информации - вы должны позаботиться об этом в своей программе сами. Файлы отличаются
от обычных массивов тем, что
- они могут изменять свой размер;
- обращение к элементам этих массивов производится не при помощи операции индекса-
ции [], а при помощи специальных системных вызовов и функций;
- доступ к элементам файла происходит в так называемой "позиции чтения/записи",
которая автоматически продвигается при операциях чтения/записи, т.е. файл прос-
матривается последовательно. Есть, правда, функции для произвольного изменения
этой позиции.
Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и
простых файлов. Об этом и о системе именования файлов прочитайте в документации по
UNIX.
4.1. Для работы с каким-либо файлом наша программа должна открыть этот файл - уста-
новить связь между именем файла и некоторой переменной в программе. При открытии
файла в ядре операционной системы выделяется "связующая" структура file "открытый
файл", содержащая:
f_offset:
указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как
RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции
чтения/записи;
f_flag:
режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные
флаги;
f_inode:
расположение файла на диске (в UNIX - в виде ссылки на I-узел файла[*]);
и кое-что еще.
У каждого процесса имеется таблица открытых им файлов - это массив ссылок на
упомянутые "связующие" структуры[**]. При открытии файла в этой таблице ищется
____________________
[*] I-узел (I-node, индексный узел) - своеобразный "паспорт", который есть у каждого
файла (в том числе и каталога). В нем содержатся:
- длина файла long di_size;
- номер владельца файла int di_uid;
- коды доступа и тип файла ushort di_mode;
- время создания и последней модификации
time_t di_ctime, di_mtime;
- начало таблицы блоков файла char di_addr[...];
- количество имен файла short di_nlink;
и.т.п.
Содержимое некоторых полей этого паспорта можно узнать вызовом stat(). Все I-узлы
собраны в единую область в начале файловой системы - так называемый I-файл. Все I-
узлы пронумерованы, начиная с номера 1. Корневой каталог (файл с именем "/") как
правило имеет I-узел номер 2.
[**] У каждого процесса в UNIX также есть свой "паспорт". Часть этого паспорта нахо-
дится в таблице процессов в ядре ОС, а часть - "приклеена" к самому процессу, однако
не доступна из программы непосредственно. Эта вторая часть паспорта носит название
"u-area" или структура user. В нее, в частности, входят таблица открытых процессом
файлов
А. Богатырев, 1992-95 - 138 - Си в UNIX
свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и
ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого
"дескриптора файла".
При закрытии файла связная структура в ядре уничтожается, ячейка в таблице счи-
тается свободной, т.е. связь программы и файла разрывается.
Дескрипторы являются локальными для каждой программы. Т.е. если две программы
открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно
совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных програм-
мах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь:
несколько или один процессов могут открыть один и тот же файл одновременно несколько
раз. При этом будет создано несколько "связующих" структур (по одной для каждого
открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуа-
ция, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание
вызова dup2.
fd u_ofile[] struct file
0 ## -------------
1---##---------------->| f_flag |
2 ## | f_count=3 |
3---##---------------->| f_inode---------*
... ## *-------------->| f_offset | |
процесс1 | ------!------ |
| ! V
0 ## | struct file ! struct inode
1 ## | ------------- ! -------------
2---##-* | f_flag | ! | i_count=2 |
3---##--->| f_count=1 | ! | i_addr[]----*
... ## | f_inode----------!-->| ... | | адреса
процесс2 | f_offset | ! ------------- | блоков
-------!----- *=========* | файла
! ! V
0 ! указатели R/W ! i_size-1
@@@@@@@@@@@!@@@@@@@@@@@@@@@@@@@@@!@@@@@@
файл на диске
/* открыть файл */
int fd = open(char имя_файла[], int как_открыть);
... /* какие-то операции с файлом */
close(fd); /* закрыть */
Параметр как_открыть:
#include <fcntl.h>
O_RDONLY - только для чтения.
O_WRONLY - только для записи.
O_RDWR - для чтения и записи.
O_APPEND - иногда используется вместе с
открытием для записи, "добавление" в файл:
O_WRONLY|O_APPEND, O_RDWR|O_APPEND
Если файл еще не существовал, то его нельзя открыть: open вернет значение (-1),
____________________
struct file *u_ofile[NOFILE];
ссылка на I-узел текущего каталога
struct inode *u_cdir;
а также ссылка на часть паспорта в таблице процессов
struct proc *u_procp;
А. Богатырев, 1992-95 - 139 - Си в UNIX
сигнализирующее об ошибке. В этом случае файл надо создать:
int fd = creat(char имя_файла[], int коды_доступа);
Дескриптор fd будет открыт для записи в этот новый пустой файл. Если же файл уже
существовал, creat опустошает его, т.е. уничтожает его прежнее содержимое и делает
его длину равной 0L байт. Коды_доступа задают права пользователей на доступ к файлу.
Это число задает битовую шкалу из 9и бит, соответствующих строке
биты: 876 543 210
rwx rwx rwx
r - можно читать файл
w - можно записывать в файл
x - можно выполнять программу из этого файла
Первая группа - эта права владельца файла, вторая - членов его группы, третяя - всех
прочих. Эти коды для владельца файла имеют еще и мнемонические имена (используемые в
вызове stat):
#include <sys/stat.h> /* Там определено: */
#define S_IREAD 0400
#define S_IWRITE 0200
#define S_IEXEC 0100
Подробности - в руководствах по системе UNIX. Отметим в частности, что open() может
вернуть код ошибки fd < 0 не только в случае, когда файл не существует
(errno==ENOENT), но и в случае, когда вам не разрешен соответствующий доступ к этому
файлу (errno==EACCES; про переменную кода ошибки errno см. в главе "Взаимодействие с
UNIX").
Вызов creat - это просто разновидность вызова open в форме
fd = open( имя_файла,
O_WRONLY|O_TRUNC|O_CREAT, коды_доступа);
O_TRUNC
означает, что если файл уже существует, то он должен быть опустошен при откры-
тии. Коды доступа и владелец не изменяются.
O_CREAT
означает, что файл должен быть создан, если его не было (без этого флага файл не
создастся, а open вернет fd < 0). Этот флаг требует задания третьего аргумента
коды_доступа[*]. Если файл уже существует - этот флаг не имеет никакого эффекта,
но зато вступает в действие O_TRUNC.
Существует также флаг
O_EXCL
который может использоваться совместно с O_CREAT. Он делает следующее: если
файл уже существует, open вернет код ошибки (errno==EEXIST). Если файл не
____________________
[*] Заметим, что на самом деле коды доступа у нового файла будут равны
di_mode = (коды_доступа & ~u_cmask) | IFREG;
(для каталога вместо IFREG будет IFDIR), где маска u_cmask задается системным вызовом
umask(u_cmask);
(вызов выдает прежнее значение маски) и в дальнейшем наследуется всеми потомками дан-
ного процесса (она хранится в u-area процесса). Эта маска позволяет запретить доступ
к определенным операциям для всех создаваемых нами файлов, несмотря на явно заданные
коды доступа, например
umask(0077); /* ???------ */
делает значащими только первые 3 бита кодов доступа (для владельца файла). Остальные
биты будут равны нулю.
Все это относится и к созданию каталогов вызовом mkdir.
А. Богатырев, 1992-95 - 140 - Си в UNIX
существовал - срабатывает O_CREAT и файл создается. Это позволяет предохранить
уже существующие файлы от уничтожения.
Файл удаляется при помощи
int unlink(char имя_файла[]);
У каждой программы по умолчанию открыты три первых дескриптора, обычно связанные
0 - с клавиатурой (для чтения)
1 - с дисплеем (выдача результатов)
2 - с дисплеем (выдача сообщений об ошибках)
Если при вызове close(fd) дескриптор fd не соответствует открытому файлу (не был отк-
рыт) - ничего не происходит.
Часто используется такая метафора: если представлять себе файлы как книжки
(только чтение) и блокноты (чтение и запись), стоящие на полке, то открытие файла -
это выбор блокнота по заглавию на его обложке и открытие обложки (на первой стра-
нице). Теперь можно читать записи, дописывать, вычеркивать и править записи в сере-
дине, листать книжку! Страницы можно сопоставить блокам файла (см. ниже), а "полку"
с книжками - каталогу.
4.2. Напишите программу, которая копирует содержимое одного файла в другой (новый)
файл. При этом используйте системные вызовы чтения и записи read и write. Эти сис-
вызовы пересылают массивы байт из памяти в файл и наоборот. Но любую переменную можно
рассматривать как массив байт, если забыть о структуре данных в переменной!
Читайте и записывайте файлы большими кусками, кратными 512 байтам. Это уменьшит
число обращений к диску. Схема:
char buffer[512]; int n; int fd_inp, fd_outp;
...
while((n = read (fd_inp, buffer, sizeof buffer)) > 0)
write(fd_outp, buffer, n);
Приведем несколько примеров использования write:
char c = 'a';
int i = 13, j = 15;
char s[20] = "foobar";
char p[] = "FOOBAR";
struct { int x, y; } a = { 666, 999 };
/* создаем файл с доступом rw-r--r-- */
int fd = creat("aFile", 0644);
write(fd, &c, 1);
write(fd, &i, sizeof i); write(fd, &j, sizeof(int));
write(fd, s, strlen(s)); write(fd, &a, sizeof a);
write(fd, p, sizeof(p) - 1);
close(fd);
Обратите внимание на такие моменты:
- При использовании write() и read() надо передавать АДРЕС данного, которое мы
хотим записать в файл (места, куда мы хотим прочитать данные из файла).
- Операции read и write возвращают число действительно прочитанных/записанных байт
(при записи оно может быть меньше указанного нами, если на диске не хватает
места; при чтении - если от позиции чтения до конца файла содержится меньше
информации, чем мы затребовали).
- Операции read/write продвигают указатель чтения/записи
RWptr += прочитанное_или_записанное_число_байт;
При открытии файла указатель стоит на начале файла: RWptr=0. При записи файл
А. Богатырев, 1992-95 - 141 - Си в UNIX
если надо автоматически увеличивает свой размер. При чтении - если мы достигнем
конца файла, то read будет возвращать "прочитано 0 байт" (т.е. при чтении указа-
тель чтения не может стать больше размера файла).
- Аргумент сколькоБайт имеет тип unsigned, а не просто int:
int n = read (int fd, char *адрес, unsigned сколькоБайт);
int n = write(int fd, char *адрес, unsigned сколькоБайт);
Приведем упрощенные схемы логики этих сисвызовов, когда они работают с обычным диско-
вым файлом (в UNIX устройства тоже выглядят для программ как файлы, но иногда с осо-
быми свойствами):
4.2.1. m = write(fd, addr, n);
если( ФАЙЛ[fd] не открыт на запись) то вернуть (-1);
если(n == 0) то вернуть 0;
если( ФАЙЛ[fd] открыт на запись с флагом O_APPEND ) то
RWptr = длина_файла; /* т.е. встать на конец файла */
если( RWptr > длина_файла ) то
заполнить нулями байты файла в интервале
ФАЙЛ[fd][ длина_файла..RWptr-1 ] = '\0';
скопировать байты из памяти процесса в файл
ФАЙЛ[fd][ RWptr..RWptr+n-1 ] = addr[ 0..n-1 ];
отводя на диске новые блоки, если надо
RWptr += n;
если( RWptr > длина_файла ) то
длина_файла = RWptr;
вернуть n;
4.2.2. m = read(fd, addr, n);
если( ФАЙЛ[fd] не открыт на чтение) то вернуть (-1);
если( RWptr >= длина_файла ) то вернуть 0;
m = MIN( n, длина_файла - RWptr );
скопировать байты из файла в память процесса
addr[ 0..m-1 ] = ФАЙЛ[fd][ RWptr..RWptr+m-1 ];
RWptr += m;
вернуть m;
4.3. Найдите ошибки в фрагменте программы:
#define STDOUT 1 /* дескриптор стандартного вывода */
int i;
static char s[20] = "hi\n";
char c = '\n';
struct a{ int x,y; char ss[5]; } po;
scanf( "%d%d%d%s%s", i, po.x, po.y, s, po.ss);
write( STDOUT, s, strlen(s));
write( STDOUT, c, 1 ); /* записать 1 байт */
Ответ: в функции scanf перед аргументом i должна стоять операция "адрес", то есть &i.
Аналогично про &po.x и &po.y. Заметим, что s - это массив, т.е. s и так есть адрес,
поэтому перед s операция & не нужна; аналогично про po.ss - здесь & не требуется.
В системном вызове write второй аргумент должен быть адресом данного, которое мы
хотим записать в файл. Поэтому мы должны были написать &c (во втором вызове write).
Ошибка в scanf - указание значения переменной вместо ее адреса - является
довольно распространенной и не может быть обнаружена компилятором (даже при использо-
вании прототипа функции scanf(char *fmt, ...), так как scanf - функция с переменным
А. Богатырев, 1992-95 - 142 - Си в UNIX
числом аргументов заранее не определенных типов). Приходится полагаться исключительно
на собственную внимательность!
4.4. Как по дескриптору файла узнать, открыт он на чтение, запись, чтение и запись
одновременно? Вот два варианта решения:
#include <fcntl.h>
#include <stdio.h>
#include <sys/param.h> /* там определено NOFILE */
#include <errno.h>
char *typeOfOpen(fd){
int flags;
if((flags=fcntl (fd, F_GETFL, NULL)) < 0 )
return NULL; /* fd вероятно не открыт */
flags &= O_RDONLY | O_WRONLY | O_RDWR;
switch(flags){
case O_RDONLY: return "r";
case O_WRONLY: return "w";
case O_RDWR: return "r+w";
default: return NULL;
}
}
char *type2OfOpen(fd){
extern errno; /* см. главу "системные вызовы" */
int r=1, w=1;
errno = 0; read(fd, NULL, 0);
if( errno == EBADF ) r = 0;
errno = 0; write(fd, NULL, 0);
if( errno == EBADF ) w = 0;
return (w && r) ? "r+w" :
w ? "w" :
r ? "r" :
"closed";
}
main(){
int i; char *s, *p;
for(i=0; i < NOFILE; i++ ){
printf("%d:%s %s\n", i, s? s: "closed", p);
}
}
Константа NOFILE означает максимальное число одновременно открытых файлов для одного
процесса (это размер таблицы открытых процессом файлов, таблицы дескрипторов). Изу-
чите описание системного вызова fcntl (file control).
4.5. Напишите функцию rename() для переименования файла. Указание: используйте сис-
темные вызовы link() и unlink(). Ответ:
А. Богатырев, 1992-95 - 143 - Си в UNIX
rename( from, to )
char *from, /* старое имя */
*to; /* новое имя */
{
unlink( to ); /* удалить файл to */
if( link( from, to ) < 0 ) /* связать */
return (-1);
unlink( from ); /* стереть старое имя */
return 0; /* OK */
}
Вызов
link(существующее_имя, новое_имя);
создает файлу альтернативное имя - в UNIX файл может иметь несколько имен: так каждый
каталог имеет какое-то имя в родительском каталоге, а также имя "." в себе самом.
Каталог же, содержащий подкаталоги, имеет некоторое имя в своем родительском ката-
логе, имя "." в себе самом, и по одному имени ".." в каждом из своих подкаталогов.
Этот вызов будет неудачен, если файл новое_имя уже существует; а также если мы
попытаемся создать альтернативное имя в другой файловой системе. Вызов
unlink(имя_файла)
удаляет имя файла. Если файл больше не имеет имен - он уничтожается. Здесь есть одна
тонкость: рассмотрим фрагмент
int fd;
close(creat("/tmp/xyz", 0644)); /*Создать пустой файл*/
fd = open("/tmp/xyz", O_RDWR);
unlink("/tmp/xyz");
...
close(fd);
Первый оператор создает пустой файл. Затем мы открываем файл и уничтожаем его
единственное имя. Но поскольку есть программа, открывшая этот файл, он не удаляется
немедленно! Программа далее работает с безымянным файлом при помощи дескриптора fd.
Как только файл закрывается - он будет уничтожен системой (как не имеющий имен).
Такой трюк используется для создания временных рабочих файлов.
Файл можно удалить из каталога только в том случае, если данный каталог имеет
для вас код доступа "запись". Коды доступа самого файла при удалении не играют роли.
В современных версиях UNIX есть системный вызов rename, который делает то же
самое, что и написанная нами одноименная функция.
4.6. Существование альтернативных имен у файла позволяет нам решить некоторые проб-
лемы, которые могут возникнуть при использовании чужой программы, от которой нет
исходного текста (которую нельзя поправить). Пусть программа выдает некоторую инфор-
мацию в файл zz.out (и это имя жестко зафиксировано в ней, и не задается через аргу-
менты программы):
/* Эта программа компилируется в a.out */
main(){
int fd = creat("zz.out", 0644);
write(fd, "It's me\n", 8);
}
Мы же хотим получить вывод на терминал, а не в файл. Очевидно, мы должны сделать файл
zz.out синонимом устройства /dev/tty (см. конец этой главы). Это можно сделать коман-
дой ln:
$ rm zz.out ; ln /dev/tty zz.out
$ a.out
$ rm zz.out
или программно:
А. Богатырев, 1992-95 - 144 - Си в UNIX
/* Эта программа компилируется в start */
/* и вызывается вместо a.out */
#include <stdio.h>
main(){
unlink("zz.out");
link("/dev/tty", "zz.out");
if( !fork()){ execl("a.out", NULL); }
else wait(NULL);
unlink("zz.out");
}
(про fork, exec, wait смотри в главе про UNIX).
Еще один пример: программа a.out желает запустить программу /usr/bin/vi (смотри
про функцию system() сноску через несколько страниц):
main(){
... system("/usr/bin/vi xx.c"); ...
}
На вашей же машине редактор vi помещен в /usr/local/bin/vi. Тогда вы просто создаете
альтернативное имя этому редактору:
$ ln /usr/local/bin/vi /usr/bin/vi
Помните, что альтернативное имя файлу можно создать лишь в той же файловой системе,
где содержится исходное имя. В семействе BSD [*] это ограничение можно обойти, создав
"символьную ссылку" вызовом
symlink(link_to_filename,link_file_name_to_be_created);
Символьная ссылка - это файл, содержащий имя другого файла (или каталога). Система
не производит автоматический подсчет числа таких ссылок, поэтому возможны "висячие"
ссылки - указывающие на уже удаленный файл. Прочесть содержимое файла-ссылки можно
системным вызовом
char linkbuf[ MAXPATHLEN + 1]; /* куда поместить ответ */
int len = readlink(pathname, linkbuf, sizeof linkbuf);
linkbuf[len] = '\0';
Системный вызов stat автоматически разыменовывает символьные ссылки и выдает информа-
цию про указуемый файл. Системный вызов lstat (аналог stat за исключением названия)
выдает информацию про саму ссылку (тип файла S_IFLNK). Коды доступа к ссылке не
имеют никакого значения для системы, существенны только коды доступа самого указуе-
мого файла.
Еще раз: символьные ссылки удобны для указания файлов и каталогов на другом
диске. Пусть у вас не помещается на диск каталог /opt/wawa. Вы можете разместить
каталог wawa на диске USR: /usr/wawa. После чего создать символьную ссылку из /opt:
ln -s /usr/wawa /opt/wawa
чтобы программы видели этот каталог под его прежним именем /opt/wawa.
Еще раз:
hard link
- то, что создается системным вызовом link, имеет тот же I-node (индексный узел,
паспорт), что и исходный файл. Это просто альтернативное имя файла, учитываемое
в поле di_nlink в I-node.
____________________
[*] BSD - семейство UNIX-ов из University of California, Berkley. Berkley Software
Distribution.
А. Богатырев, 1992-95 - 145 - Си в UNIX
symbolic link
- создается вызовом symlink. Это отдельный самостоятельный файл, с собственным
I-node. Правда, коды доступа к этому файлу не играют никакой роли; значимы
только коды доступа указуемого файла.
4.7. Напишите программу, которая находит в файле символ @ и выдает файл с этого
места дважды. Указание: для запоминания позиции в файле используйте вызов lseek() -
позиционирование указателя чтения/записи:
long offset, lseek();
...
/* Узнать текущую позицию чтения/записи:
* сдвиг на 0 от текущей позиции. lseek вернет новую
* позицию указателя (в байтах от начала файла). */
offset = lseek(fd, 0L, 1); /* ftell(fp) */
А для возврата в эту точку:
lseek(fd, offset, 0); /* fseek(fp, offset, 0) */
По поводу lseek надо помнить такие вещи:
- lseek(fd, offset, whence) устанавливает указатель чтения/записи на расстояние
offset байт
при whence:
0 от начала файла RWptr = offset;
1 от текущей позиции RWptr += offset;
2 от конца файла RWptr = длина_файла + offset;
Эти значения whence можно обозначать именами:
#include <stdio.h>
0 это SEEK_SET
1 это SEEK_CUR
2 это SEEK_END
- Установка указателя чтения/записи - это виртуальная операция, т.е. реального
подвода магнитных головок и вообще обращения к диску она не вызывает. Реальное
движение головок к нужному месту диска произойдет только при операциях
чтения/записи read()/write(). Поэтому lseek() - дешевая операция.
- lseek() возвращает новую позицию указателя чтения/записи RWptr относительно
начала файла (long смещение в байтах). Помните, что если вы используете это зна-
чение, то вы должны предварительно описать lseek как функцию, возвращающую длин-
ное целое: long lseek();
- Аргумент offset должен иметь тип long (не ошибитесь!).
- Если поставить указатель за конец файла (это допустимо!), то операция записи
write() сначала заполнит байтом '\0' все пространство от конца файла до позиции
указателя; операция read() при попытке чтения из-за конца файла вернет "прочи-
тано 0 байт". Попытка поставить указатель перед началом файла вызовет ошибку.
- Вызов lseek() неприменим к pipe и FIFO-файлам, поэтому попытка сдвинуться на 0
байт выдаст ошибку:
/* это стандартная функция */
int isapipe(int fd){
extern errno;
return (lseek(fd, 0L, SEEK_CUR) < 0 && errno == ESPIPE);
}
выдает "истину", если fd - дескриптор "трубы"(pipe).
А. Богатырев, 1992-95 - 146 - Си в UNIX
4.8. Каков будет эффект следующей программы?
int fd = creat("aFile", 0644); /* creat создает файл
открытый на запись, с доступом rw-r--r-- */
write(fd, "begin", 5 );
lseek(fd, 1024L * 1000, 0);
write(fd, "end", 3 );
close(fd);
Напомним, что при записи в файл, его длина автоматически увеличивается, когда мы
записываем информацию за прежним концом файла. Это вызывает отведение места на диске
для хранения новых данных (порциями, называемыми блоками - размером от 1/2 до 8 Кб в
разных версиях). Таким образом, размер файла ограничен только наличием свободных
блоков на диске.
В нашем примере получится файл длиной 1024003 байта. Будет ли он занимать на
диске 1001 блок (по 1 Кб)?
В системе UNIX - нет! Вот кое-что про механику выделения блоков:
- Блоки располагаются на диске не обязательно подряд - у каждого файла есть специ-
альным образом организованная таблица адресов его блоков.
- Последний блок файла может быть занят не целиком (если длина файла не кратна
размеру блока), тем не менее число блоков у файла всегда целое (кроме семейства
BSD, где блок может делиться на фрагменты, принадлежащие разным файлам). Опера-
ционная система в каждый момент времени знает длину файла с точностью до одного
байта и не позволяет нам "заглядывать" в остаток блока, пока при своем "росте"
файл не займет эти байты.
- Блок на диске физически выделяется лишь после операции записи в этот блок.
В нашем примере: при создании файла его размер 0, и ему выделено 0 блоков. При
первой записи файлу будет выделен один блок (логический блок номер 0 для файла) и в
его начало запишется "begin". Длина файла станет равна 5 (остаток блока - 1019 байт
- не используется и файлу логически не принадлежит!). Затем lseek поставит указатель
записи далеко за конец файла и write запишет в 1000-ый блок слово "end". 1000-ый блок
будет выделен на диске. В этот момент у файла "возникнут" и все промежуточные блоки
1..999. Однако они будут только "числиться за файлом", но на диске отведены не будут
(в таблице блоков файла это обозначается адресом 0)! При чтении из них будут
читаться байты '\0'. Это так называемая "дырка" в файле. Файл имеет размер 1024003
байта, но на диске занимает всего 2 блока (на самом деле чуть больше, т.к. часть
таблицы блоков файла тоже находится в специальных блоках файла). Блок из "дырки"
станет реальным, если в него что-нибудь записать.
Будьте готовы к тому, что "размер файла" (который, кстати, можно узнать систем-
ным вызовом stat) - это в UNIX не то же самое, что "место, занимаемое файлом на
диске".
4.9. Найдите ошибки:
FILE *fp;
...
fp = open( "файл", "r" ); /* открыть */
close(fp); /* закрыть */
Ответ: используется системный вызов open() вместо функции fopen(); а также close
вместо fclose, а их форматы (и результат) различаются! Следует четко различать две
существующие в Си модели обмена с файлами: через системные вызовы: open, creat,
close, read, write, lseek; и через библиотеку буферизованного обмена stdio: fopen,
fclose, fread, fwrite, fseek, getchar, putchar, printf, и.т.д. В первой из них обра-
щение к файлу происходит по целому fd - дескриптору файла, а во втором - по указателю
FILE *fp - указателю на файл. Это параллельные механизмы (по своим возможностям),
хотя второй является просто надстройкой над первым. Тем не менее, лучше их не смеши-
вать.
А. Богатырев, 1992-95 - 147 - Си в UNIX
4.10. Доступ к диску (чтение/запись) гораздо (на несколько порядков) медленнее, чем
доступ к данным в оперативной памяти. Кроме того, если мы читаем или записываем файл
при помощи системных вызовов маленькими порциями (по 1-10 символов)
char c;
while( read(0, &c, 1)) ... ; /* 0 - стандартный ввод */
то мы проигрываем еще в одном: каждый системный вызов - это обращение к ядру операци-
онной системы. При каждом таком обращении происходит довольно большая дополнительная
работа (смотри главу "Взаимодействие с UNIX"). При этом накладные расходы на такое
посимвольное чтение файла могут значительно превысить полезную работу.
Еще одной проблемой является то, что системные вызовы работают с файлом как с
неструктурированным массивом байт; тогда как человеку часто удобнее представлять, что
файл поделен на строки, содержащие читабельный текст, состоящий лишь из обычных
печатных символов (текстовый файл).
Для решения этих двух проблем была построена специальная библиотека функций,
названная stdio - "стандартная библиотека ввода/вывода" (standard input/output
library). Она является частью библиотеки /lib/libc.a и представляет собой надстройку
над системными вызовами (т.к. в конце концов все ее функции время от времени обраща-
ются к системе, но гораздо реже, чем если использовать сисвызовы непосредственно).
Небезызвестная директива #include <stdio.h> включает в нашу программу файл с объявле-
нием форматов данных и констант, используемых этой библиотекой.
Библиотеку stdio можно назвать библиотекой буферизованного обмена, а также биб-
лиотекой работы с текстовыми файлами (т.е. имеющими разделение на строки), поскольку
для оптимизации обменов с диском (для уменьшения числа обращений к нему и тем самым
сокращения числа системных вызовов) эта библиотека вводит буферизацию, а также пре-
доставляет несколько функций для работы со строчно-организованными файлами.
Связь с файлом в этой модели обмена осуществляется уже не при помощи целого
числа - дескриптора файла (file descriptor), а при помощи адреса "связной" структуры
FILE. Указатель на такую структуру условно называют указателем на файл (file
pointer)[*]. Структура FILE содержит в себе:
- дескриптор fd файла для обращения к системным вызовам;
- указатель на буфер, размещенный в памяти программы;
- указатель на текущее место в буфере, откуда надо выдать или куда записать оче-
редной символ; этот указатель продвигается при каждом вызове getc или putc;
- счетчик оставшихся в буфере символов (при чтении) или свободного места (при
записи);
- режимы открытия файла (чтение/запись/чтение+запись) и текущее состояние файла.
Одно из состояний - при чтении файла был достигнут его конец[**];
- способ буферизации;
Предусмотрено несколько стандартных структур FILE, указатели на которые называются
stdin, stdout и stderr и связаны с дескрипторами 0, 1, 2 соответственно (стандартный
ввод, стандартный вывод, стандартный вывод ошибок). Напомним, что эти каналы открыты
неявно (автоматически) и, если не перенаправлены, связаны с вводом с клавиатуры и
выводом на терминал.
Буфер в оперативной памяти нашей программы создается (функцией malloc) при отк-
рытии файла при помощи функции fopen(). После открытия файла все операции обмена с
файлом происходят не по 1 байту, а большими порциями размером с буфер - обычно по 512
байт (константа BUFSIZ).
При чтении символа
int c; FILE *fp = ... ;
c = getc(fp);
____________________
[*] Это не та "связующая" структура file в ядре, про которую шла речь выше, а ЕЩЕ
одна - в памяти самой программы.
[**] Проверить это состояние позволяет макрос feof(fp); он истинен, если конец был
достигнут, ложен - если еще нет.
А. Богатырев, 1992-95 - 148 - Си в UNIX
в буфер считывается read-ом из файла порция информации, и getc выдает ее первый байт.
При последующих вызовах getc выдаются следующие байты из буфера, а обращений к диску
уже не происходит! Лишь когда буфер будет исчерпан - произойдет очередное чтение с
диска. Таким образом, информация читается из файла с опережением, заранее наполняя
буфер; а по требованию выдается уже из буфера. Если мы читаем 1024 байта из файла
при помощи getc(), то мы 1024 раза вызываем эту функцию, но всего 2 раза системный
вызов read - для чтения двух порций информации из файла, каждая - по 512 байт.
При записи
char c; FILE *fp = ... ;
putc(c, fp);
выводимые символы накапливаются в буфере. Только когда в нем окажется большая порция
информации, она за одно обращение write записывается на диск. Буфер записи "выталки-
вается" в файл в таких случаях:
- буфер заполнен (содержит BUFSIZ символов).
- при закрытии файла (fclose или exit [*][*]).
- при вызове функции fflush (см. ниже).
- в специальном режиме - после помещения в буфер символа '\n' (см. ниже).
- в некоторых версиях - перед любой операцией чтения из канала stdin (например,
при вызове gets), при условии, что stdout буферизован построчно (режим _IOLBF,
смотри ниже), что по-умолчанию так и есть.
Приведем упрощенную схему, поясняющую взаимоотношения основных функций и макросов из
stdio (кто кого вызывает). Далее s означает строку, c - символ, fp - указатель на
структуру FILE [**][**]. Функции, работающие со строками, в цикле вызывают посимвольные
операции. Обратите внимание, что в конце концов все функции обращаются к системным
вызовам read и write, осуществляющим ввод/вывод низкого уровня.
Системные вызовы далее обозначены жирно, макросы - курсивом.
Открыть файл, создать буфер:
#include <stdio.h>
FILE *fp = fopen(char *name, char *rwmode);
| вызывает
V
int fd = open (char *name, int irwmode);
Если открываем на запись и файл не существует (fd < 0),
то создать файл вызовом:
fd = creat(char *name, int accessmode);
fd будет открыт для записи в файл.
По умолчанию fopen() использует для creat коды доступа accessmode равные 0666 (rw-
rw-rw-).
____________________
[*][*] При выполнении вызова завершения программы exit(); все открытые файлы автомати-
чески закрываются.
[**][**] Обозначения fd для дескрипторов и fp для указателей на файл прижились и их сле-
дует придерживаться. Если переменная должна иметь более мнемоничное имя - следует
писать так: fp_output, fd_input (а не просто fin, fout).
А. Богатырев, 1992-95 - 149 - Си в UNIX
Соответствие аргументов fopen и open:
rwmode irwmode
-------------------------
"r" O_RDONLY
"w" O_WRONLY|O_CREAT |O_TRUNC
"r+" O_RDWR
"w+" O_RDWR |O_CREAT |O_TRUNC
"a" O_WRONLY|O_CREAT |O_APPEND
"a+" O_RDWR |O_CREAT |O_APPEND
Для r, r+ файл уже должен существовать, в остальных случаях файл создается, если его
не было.
Если fopen() не смог открыть (или создать) файл, он возвращает значение NULL:
if((fp = fopen(name, rwmode)) == NULL){ ...неудача... }
Итак, схема:
printf(fmt,...)--->--,----fprintf(fp,fmt,...)->--*
fp=stdout |
fputs(s,fp)--------->--|
puts(s)----------->-------putchar(c)-----,---->--|
fp=stdout |
fwrite(array,size,count,fp)->--|
|
Ядро ОС putc(c,fp)
------------------* |
|файловая---<--write(fd,s,len)------------<----БУФЕР
|система---->---read(fd,s,len)-* _flsbuf(c,fp)
| | ! |
|системные буфера ! |
| | ! V ungetc(c,fp)
|драйвер устр-ва ! | |
|(диск, терминал) ! | _filbuf(fp) |
| | ! *--------->-----БУФЕР<-*
|устройство ! |
------------------* c=getc(fp)
|
rdcount=fread(array,size,count,fp)--<--|
gets(s)-------<---------c=getchar()------,----<--|
fp=stdout |
|
fgets(sbuf,buflen,fp)-<--|
scanf(fmt,.../*ук-ли*/)--<-,--fscanf(fp,fmt,...)-*
fp=stdin
Закрыть файл, освободить память выделенную под буфер:
fclose(fp) ---> close(fd);
И чуть в стороне - функция позиционирования:
fseek(fp,long_off,whence) ---> lseek(fd,long_off,whence);
Функции _flsbuf и _filbuf - внутренние для stdio, они как раз сбрасывают буфер в файл
либо читают новый буфер из файла.
По указателю fp можно узнать дескриптор файла:
int fd = fileno(fp);
Это макроопределение просто выдает поле из структуры FILE. Обратно, если мы открыли
А. Богатырев, 1992-95 - 150 - Си в UNIX
файл open-ом, мы можем ввести буферизацию этого канала:
int fd = open(name, O_RDONLY); /* или creat() */
...
FILE *fp = fdopen(fd, "r");
(здесь надо вновь указать КАК мы открываем файл, что должно соответствовать режиму
открытия open-ом). Теперь можно работать с файлом через fp, а не fd.
В приложении имеется текст, содержащий упрощенную реализацию главных функций из
библиотеки stdio.
4.11. Функция ungetc(c,fp) "возвращает" прочитанный байт в файл. На самом деле байт
возвращается в буфер, поэтому эта операция неприменима к небуферизованным каналам.
Возврат соответствует сдвигу указателя чтения из буфера (который увеличивается при
getc()) на 1 позицию назад. Вернуть можно только один символ подряд (т.е. перед сле-
дующим ungetc-ом должен быть хоть один getc), поскольку в противном случае можно
сдвинуть указатель за начало буфера и, записывая туда символ c, разрушить память
программы.
while((c = getchar()) != '+' );
/* Прочли '+' */ ungetc(c ,stdin);
/* А можно заменить этот символ на другой! */
c = getchar(); /* снова прочтет '+' */
4.12. Очень часто делают ошибку в функции fputc, путая порядок ее аргументов. Так
ничего не стоит написать:
FILE *fp = ......;
fputc( fp, '\n' );
Запомните навсегда!
int fputc( int c, FILE *fp );
указатель файла идет вторым! Существует также макроопределение
putc( c, fp );
Оно ведет себя как и функция fputc, но не может быть передано в качестве аргумента в
функцию:
#include <stdio.h>
putNtimes( fp, c, n, f )
FILE *fp; int c; int n; int (*f)();
{ while( n > 0 ){ (*f)( c, fp ); n--; }}
возможен вызов
putNtimes( fp, 'a', 3, fputc );
но недопустимо
putNtimes( fp, 'a', 3, putc );
Тем не менее всегда, где возможно, следует пользоваться макросом - он работает быст-
рее. Аналогично, есть функция fgetc(fp) и макрос getc(fp).
Отметим еще, что putchar и getchar это тоже всего лишь макросы
#define putchar(c) putc((c), stdout)
#define getchar() getc(stdin)
А. Богатырев, 1992-95 - 151 - Си в UNIX
4.13. Известная вам функция printf также является частью библиотеки stdio. Она вхо-
дит в семейство функций:
FILE *fp; char bf[256];
fprintf(fp, fmt, ... );
printf( fmt, ... );
sprintf(bf, fmt, ... );
Первая из функций форматирует свои аргументы в соответствии с форматом, заданным
строкой fmt (она содержит форматы в виде %-ов) и записывает строку-результат посим-
вольно (вызывая putc) в файл fp. Вторая - это всего-навсего fprintf с каналом fp
равным stdout. Третяя выдает сформатированную строку не в файл, а записывает ее в
массив bf. В конце строки sprintf добавляет нулевой байт '\0' - признак конца.
Для чтения данных по формату используются функции семейства
fscanf(fp, fmt, /* адреса арг-тов */...);
scanf( fmt, ... );
sscanf(bf, fmt, ... );
Функции fprintf и fscanf являются наиболее мощным средством работы с текстовыми фай-
лами (содержащими изображение данных в виде печатных символов).
4.14. Текстовые файлы (имеющие строчную организацию) хранятся на диске как линейные
массивы байт. Для разделения строк в них используется символ '\n'. Так, например,
текст
стр1
стрк2
кнц
хранится как массив
с т р 1 \n с т р к 2 \n к н ц длина=14 байт
!
указатель чтения/записи (read/write pointer RWptr)
(расстояние в байтах от начала файла)
При выводе на экран дисплея символ \n преобразуется драйвером терминалов в последова-
тельность \r\n, которая возвращает курсор в начало строки ('\r') и опускает курсор на
строку вниз ('\n'), то есть курсор переходит в начало следующей строки.
В MS DOS строки в файле на диске разделяются двумя символами \r\n и при выводе
на экран никаких преобразований не делается[*]. Зато библиотечные функции языка Си
преобразуют эту последовательность при чтении из файла в \n, а при записи в файл
превращают \n в \r\n, поскольку в Си считается, что строки разделяются только \n. Для
работы с файлом без таких преобразований, его надо открывать как "бинарный":
FILE *fp = fopen( имя, "rb" ); /* b - binary */
int fd = open ( имя, O_RDONLY | O_BINARY );
____________________
[*] Управляющие символы имеют следующие значения:
'\n' - '\012' (10) line feed
'\r' - '\015' (13) carriage return
'\t' - '\011' (9) tab
'\b' - '\010' (8) backspace
'\f' - '\014' (12) form feed
'\a' - '\007' (7) audio bell (alert)
'\0' - 0. null byte
А. Богатырев, 1992-95 - 152 - Си в UNIX
Все нетекстовые файлы в MS DOS надо открывать именно так, иначе могут произойти раз-
ные неприятности. Например, если мы программой копируем нетекстовый файл в текстовом
режиме, то одиночный символ \n будет считан в программу как \n, но записан в новый
файл как пара \r\n. Поэтому новый файл будет отличаться от оригинала (что для файлов
с данными и программ совершенно недопустимо!).
Задание: напишите программу подсчета строк и символов в файле. Указание: надо
подсчитать число символов '\n' в файле и учесть, что последняя строка файла может не
иметь этого символа на конце. Поэтому если последний символ файла (тот, который вы
прочитаете самым последним) не есть '\n', то добавьте к счетчику строк 1.
4.15. Напишите программу подсчета количества вхождений каждого из символов алфавита
в файл и печатающую результат в виде таблицы в 4 колонки. (Указание: заведите массив
из 256 счетчиков. Для больших файлов счетчики должны быть типа long).
4.16. Почему вводимый при помощи функций getchar() и getc(fp) символ должен описы-
ваться типом int а не char?
Ответ: функция getchar() сообщает о конце файла тем, что возвращает значение EOF
(end of file), равное целому числу (-1). Это НЕ символ кодировки ASCII, поскольку
getchar() может прочесть из файла любой символ кодировки (кодировка содержит символы
с кодами 0...255), а специальный признак не должен совпадать ни с одним из хранимых в
файле символов. Поэтому для его хранения требуется больше одного байта (нужен хотя
бы еще 1 бит). Проверка на конец файла в программе обычно выглядит так:
...
while((ch = getchar()) != EOF ){
putchar(ch);
...
}
- Пусть ch имеет тип unsigned char. Тогда ch всегда лежит в интервале 0...255 и
НИКОГДА не будет равно (-1). Даже если getchar() вернет такое значение, оно
будет приведено к типу unsigned char обрубанием и станет равным 255. При срав-
нении с целым (-1) оно расширится в int добавлением нулей слева и станет равно
255. Таким образом, наша программа никогда не завершится, т.к. вместо признака
конца файла она будет читать символ с кодом 255 (255 != -1).
- Пусть ch имеет тип signed char. Тогда перед сравнением с целым числом EOF байт
ch будет приведен к типу signed int при помощи расширения знакового бита (7-
ого). Если getchar вернет значение (-1), то оно будет сначала в присваивании
значения байту ch обрублено до типа char: 255; но в сравнении с EOF значение 255
будет приведено к типу int и получится (-1). Таким образом, истинный конец файла
будет обнаружен. Но теперь, если из файла будет прочитан настоящий символ с
кодом 255, он будет приведен в сравнении к целому значению (-1) и будет также
воспринят как конец файла. Таким образом, если в нашем файле окажется символ с
кодом 255, то программа воспримет его как фальшивый конец файла и оставит весь
остаток файла необработанным (а в нетекстовых файлах такие символы - не ред-
кость).
- Пусть ch имеет тип int или unsigned int (больше 8 бит). Тогда все корректно.
Отметим, что в UNIX признак конца файла в самом файле физически НЕ ХРАНИТСЯ. Система
в любой момент времени знает длину файла с точностью до одного байта; признак EOF
вырабатывается стандартными функциями тогда, когда обнаруживается, что указатель чте-
ния достиг конца файла (то есть позиция чтения стала равной длине файла - последний
байт уже прочитан).
В MS DOS же в текстовых файлах признак конца (EOF) хранится явно и обозначается
символом CTRL/Z. Поэтому, если программным путем записать куда-нибудь в середину
файла символ CTRL/Z, то некоторые программы перестанут "видеть" остаток файла после
этого символа!
Наконец отметим, что разные функции при достижении конца файла выдают разные
значения: scanf, fscanf, fgetc, getc, getchar выдают EOF, read - выдает 0, а gets,
fgets - NULL.
А. Богатырев, 1992-95 - 153 - Си в UNIX
4.17. Напишите программу, которая запрашивает ваше имя и приветствует вас. Для ввода
имени используйте стандартные библиотечные функции
gets(s);
fgets(s,slen,fp);
В чем разница?
Ответ: функция gets() читает строку (завершающуюся '\n') из канала fp==stdin.
Она не контролирует длину буфера, в которую считывается строка, поэтому если строка
окажется слишком длинной - ваша программа повредит свою память (и аварийно завер-
шится). Единственный возможный совет - делайте буфер достаточно большим (очень туман-
ное понятие!), чтобы вместить максимально возможную (длинную) строку.
Функция fgets() контролирует длину строки: если строка на входе окажется длин-
нее, чем slen символов, то остаток строки не будет прочитан в буфер s, а будет остав-
лен "на потом". Следующий вызов fgets прочитает этот сохраненный остаток. Кроме того
fgets, в отличие от gets, не обрубает символ '\n' на конце строки, что доставляет нам
дополнительные хлопоты по его уничтожению, поскольку в Си "нормальные" строки завер-
шаются просто '\0', а не "\n\0".
char buffer[512]; FILE *fp = ... ; int len;
...
while(fgets(buffer, sizeof buffer, fp)){
if((len = strlen(buffer)) && buffer[len-1] == '\n')
/* @ */ buffer[--len] = '\0';
printf("%s\n", buffer);
}
Здесь len - длина строки. Если бы мы выбросили оператор, помеченный '@', то printf
печатал бы текст через строку, поскольку выдавал бы код '\n' дважды - из строки
buffer и из формата "%s\n".
Если в файле больше нет строк (файл дочитан до конца), то функции gets и fgets
возвращают значение NULL. Обратите внимание, что NULL, а не EOF. Пока файл не дочи-
тан, эти функции возвращают свой первый аргумент - адрес буфера, в который была запи-
сана очередная строка файла.
Фрагмент для обрубания символа перевода строки может выглядеть еще так:
#include <stdio.h>
#include <string.h>
char buffer[512]; FILE *fp = ... ;
...
while(fgets(buffer, sizeof buffer, fp) != NULL){
char *sptr;
if(sptr = strchr(buffer, '\n'))
*sptr = '\0';
printf("%s\n", buffer);
}
4.18. В чем отличие puts(s); и fputs(s,fp); ?
Ответ: puts выдает строку s в канал stdout. При этом puts выдает сначала строку
s, а затем - дополнительно - символ перевода строки '\n'. Функция же fputs символ
перевода строки не добавляет. Упрощенно:
fputs(s, fp) char *s; FILE *fp;
{ while(*s) putc(*s++, fp); }
puts(s) char *s;
{ fputs(s, stdout); putchar('\n'); }
А. Богатырев, 1992-95 - 154 - Си в UNIX
4.19. Найдите ошибки в программе:
#include <stdio.h>
main() {
int fp;
int i;
char str[20];
fp = fopen("файл");
fgets(stdin, str, sizeof str);
for( i = 0; i < 40; i++ );
fputs(fp, "Текст, выводимый в файл:%s",str );
fclose("файл");
}
Мораль: надо быть внимательнее к формату вызова и смыслу библиотечных функций.
4.20. Напишите программу, которая распечатывает самую длинную строку из файла ввода
и ее длину.
4.21. Напишите программу, которая выдает n-ую строку файла. Номер строки и имя
файла задаются как аргументы main().
4.22. Напишите программу
slice -сКакой +сколько файл
которая выдает сколько строк файла файл, начиная со строки номер сКакой (нумерация
строк с единицы).
#include <stdio.h>
#include <ctype.h>
long line, count, nline, ncount; /* нули */
char buf[512];
void main(int argc, char **argv){
char c; FILE *fp;
argc--; argv++;
/* Разбор ключей */
while((c = **argv) == '-' || c == '+'){
long atol(), val; char *s = &(*argv)[1];
if( isdigit(*s)){
val = atol(s);
if(c == '-') nline = val;
else ncount = val;
} else fprintf(stderr,"Неизвестный ключ %s\n", s-1);
argc--; ++argv;
}
if( !*argv ) fp = stdin;
else if((fp = fopen(*argv, "r")) == NULL){
fprintf(stderr, "Не могу читать %s\n", *argv);
exit(1);
}
for(line=1, count=0; fgets(buf, sizeof buf, fp); line++){
if(line >= nline){
fputs(buf, stdout); count++;
}
if(ncount && count == ncount)
break;
}
А. Богатырев, 1992-95 - 155 - Си в UNIX
fclose(fp); /* это не обязательно писать явно */
}
/* End_Of_File */
4.23. Составьте программу, которая распечатывает последние n строк файла ввода.
4.24. Напишите программу, которая делит входной файл на файлы по n строк в каждом.
4.25. Напишите программу, которая читает 2 файла и печатает их вперемежку: одна
строка из первого файла, другая - из второго. Придумайте, как поступить, если файлы
содержат разное число строк.
4.26. Напишите программу сравнения двух файлов, которая будет печатать первую из
различающихся строк и позицию символа, в котором они различаются.
4.27. Напишите программу для интерактивной работы с файлом. Сначала у вас запраши-
вается имя файла, а затем вам выдается меню:
1. Записать текст в файл.
2. Дописать текст к концу файла.
3. Просмотреть файл.
4. Удалить файл.
5. Закончить работу.
Текст вводится в файл построчно с клавиатуры. Конец ввода - EOF (т.е. CTRL/D), либо
одиночный символ '.' в начале строки. Выдавайте число введенных строк.
Просмотр файла должен вестись постранично: после выдачи очередной порции строк
выдавайте подсказку
--more-- _
(курсор остается в той же строке и обозначен подчерком) и ожидайте нажатия клавиши.
Ответ 'q' завершает просмотр. Если файл, который вы хотите просмотреть, не сущест-
вует - выдавайте сообщение об ошибке.
После выполнения действия программа вновь запрашивает имя файла. Если вы отве-
тите вводом пустой строки (сразу нажмете <ENTER>, то должно использоваться имя файла,
введенное на предыдущем шаге. Имя файла, предлагаемое по умолчанию, принято писать в
запросе в [] скобках.
Введите имя файла [oldfile.txt]: _
Когда вы научитесь работать с экраном дисплея (см. главу "Экранные библиотеки"),
перепишите меню и выдачу сообщений с использованием позиционирования курсора в задан-
ное место экрана и с выделением текста инверсией. Для выбора имени файла предложите
меню: отсортированный список имен всех файлов текущего каталога (по поводу получения
списка файлов см. главу про взаимодействие с UNIX). Просто для распечатки текущего
каталога на экране можно также использовать вызов
system("ls -x");
а для считывания каталога в программу[*]
FILE *fp = popen("ls *.c", "r");
... fgets(...,fp); ... // в цикле, пока не EOF
pclose(fp);
(в этом примере читаются только имена .c файлов).
4.28. Напишите программу удаления n-ой строки из файла; вставки строки после m-ой.
К сожалению, это возможно только путем переписывания всего файла в другое место (без
ненужной строки) и последующего его переименования.
А. Богатырев, 1992-95 - 156 - Си в UNIX
4.29. Составьте программу перекодировки текста, набитого в кодировке КОИ-8, в аль-
тернативную кодировку и наоборот. Для этого следует составить таблицу перекодировки
из 256 символов: c_new=TABLE[c_old]; Для решения обратной задачи используйте стан-
дартную функцию strchr(). Программа читает один файл и создает новый.
4.30. Напишите программу, делящую большой файл на куски заданного размера (не в
строках, а в килобайтах). Эта программа может применяться для записи слишком боль-
шого файла на дискеты (файл режется на части и записывается на несколько дискет).
#include <fcntl.h>
#include <stdio.h>
#define min(a,b) (((a) < (b)) ? (a) : (b))
#define KB 1024 /* килобайт */
#define PORTION (20L* KB) /* < 32768 */
long ONEFILESIZE = (300L* KB);
extern char *strrchr(char *, char);
extern long atol (char *);
extern errno; /* системный код ошибки */
char buf[PORTION]; /* буфер для копирования */
void main (int ac, char *av[]) {
char name[128], *s, *prog = av[0];
int cnt=0, done=0, fdin, fdout;
/* M_UNIX автоматически определяется
* компилятором в UNIX */
#ifndef M_UNIX /* т.е. MS DOS */
extern int _fmode; _fmode = O_BINARY;
/* Задает режим открытия и создания ВСЕХ файлов */
#endif
if(av[1] && *av[1] == '-'){ /* размер одного куска */
ONEFILESIZE = atol(av[1]+1) * KB; av++; ac--;
}
if (ac < 2){
fprintf(stderr, "Usage: %s [-size] file\n", prog);
exit(1);
}
if ((fdin = open (av[1], O_RDONLY)) < 0) {
fprintf (stderr, "Cannot read %s\n", av[1]); exit (2);
}
if ((s = strrchr (av[1], '.'))!= NULL) *s = '\0';
do { unsigned long sent;
sprintf (name, "%s.%d", av[1], ++cnt);
if ((fdout = creat (name, 0644)) < 0) {
fprintf (stderr, "Cannot create %s\n", name); exit (3);
}
sent = 0L; /* сколько байт переслано */
for(;;){ unsigned isRead, /* прочитано read-ом */
need = min(ONEFILESIZE - sent, PORTION);
if( need == 0 ) break;
sent += (isRead = read (fdin, buf, need));
errno = 0;
if (write (fdout, buf, isRead) != isRead &&
errno){ perror("write"); exit(4);
} else if (isRead < need){ done++; break; }
}
if(close (fdout) < 0){
perror("Мало места на диске"); exit(5);
}
printf("%s\t%lu байт\n", name, sent);
} while( !done ); exit(0);
}
А. Богатырев, 1992-95 - 157 - Си в UNIX
4.31. Напишите обратную программу, которая склеивает несколько файлов в один. Это
аналог команды cat с единственным отличием: результат выдается не в стандартный
вывод, а в файл, указанный в строке аргументов последним. Для выдачи в стандартный
вывод следует указать имя "-".
#include <fcntl.h>
#include <stdio.h>
void main (int ac, char **av){
int i, err = 0; FILE *fpin, *fpout;
if (ac < 3) {
fprintf(stderr,"Usage: %s from... to\n", av[0]);
exit(1);
}
fpout = strcmp(av[ac-1], "-") ? /* отлично от "-" */
fopen (av[ac-1], "wb") : stdout;
for (i = 1; i < ac-1; i++) {
register int c;
fprintf (stderr, "%s\n", av[i]);
if ((fpin = fopen (av[i], "rb")) == NULL) {
fprintf (stderr, "Cannot read %s\n", av[i]);
err++; continue;
}
while ((c = getc (fpin)) != EOF)
putc (c, fpout);
fclose (fpin);
}
fclose (fpout); exit (err);
}
Обе эти программы могут без изменений транслироваться и в MS DOS и в UNIX. UNIX
просто игнорирует букву b в открытии файла "rb", "wb". При работе с read мы могли бы
открывать файл как
#ifdef M_UNIX
# define O_BINARY 0
#endif
int fdin = open( av[1], O_RDONLY | O_BINARY);
4.32. Каким образом стандартный ввод переключить на ввод из заданного файла, а стан-
дартный вывод - в файл? Как проверить, существует ли файл; пуст ли он? Как надо
открывать файл для дописывания информации в конец существующего файла? Как надо отк-
рывать файл, чтобы попеременно записывать и читать тот же файл? Указание: см. fopen,
freopen, dup2, stat. Ответ про перенаправления ввода:
способ 1 (библиотечные функции)
#include <stdio.h>
...
freopen( "имя_файла", "r", stdin );
способ 2 (системные вызовы)
#include <fcntl.h>
int fd;
...
fd = open( "имя_файла", O_RDONLY );
dup2 ( fd, 0 ); /* 0 - стандартный ввод */
close( fd ); /* fd больше не нужен - закрыть
его, чтоб не занимал место в таблице */
А. Богатырев, 1992-95 - 158 - Си в UNIX
способ 3 (системные вызовы)
#include <fcntl.h>
int fd;
...
fd = open( "имя_файла", O_RDONLY );
close (0); /* 0 - стандартный ввод */
fcntl (fd, F_DUPFD, 0 ); /* 0 - стандартный ввод */
close (fd);
Это перенаправление ввода соответствует конструкции
$ a.out < имя_файла
написанной на командном языке СиШелл. Для перенаправления вывода замените 0 на 1,
stdin на stdout, open на creat, "r" на "w".
Рассмотрим механику работы вызова dup2 [*]:
new = open("файл1",...); dup2(new, old); close(new);
таблица открытых
файлов процесса
...## ##
new----##---> файл1 new---##---> файл1
## ##
old----##---> файл2 old---## файл2
## ##
0:до вызова 1:разрыв связи old с файл2
dup2() (закрытие канала old, если он был открыт)
## ##
new----##--*--> файл1 new ## *----> файл1
## | ## |
old----##--* old--##--*
## ##
2:установка old на файл1 3:после оператора close(new);
на этом dup2 завершен. дескриптор new закрыт.
Здесь файл1 и файл2 - связующие структуры "открытый файл" в ядре, о которых рассказы-
валось выше (в них содержатся указатели чтения/записи). После вызова dup2 дескрипторы
new и old ссылаются на общую такую структуру и поэтому имеют один и тот же R/W-
указатель. Это означает, что в программе new и old являются синонимами и могут
использоваться даже вперемежку:
dup2(new, old);
write(new, "a", 1);
write(old, "b", 1);
write(new, "c", 1);
запишет в файл1 строку "abc". Программа
____________________
[*] Функция
int system(char *команда);
выполняет команду, записанную в строке команда, вызывая для этого интерпретатор ко-
манд
/bin/sh -c "команда"
А. Богатырев, 1992-95 - 159 - Си в UNIX
int fd;
printf( "Hi there\n");
fd = creat( "newout", 0640 );
dup2(fd, 1); close(fd);
printf( "Hey, You!\n");
выдаст первое сообщение на терминал, а второе - в файл newout, поскольку printf
выдает данные в канал stdout, связанный с дескриптором 1.
4.33. Напишите программу, которая будет выдавать подряд в стандартный вывод все
файлы, чьи имена указаны в аргументах командной строки. Используйте argc для органи-
зации цикла. Добавьте сквозную нумерацию строк и печать номера строки.
4.34. Напишите программу, распечатывающую первую директиву препроцессора, встретив-
шуюся в файле ввода.
#include <stdio.h>
char buf[512], word[] = "#";
main(){ char *s; int len = strlen(word);
while((s=fgets(buf, sizeof buf, stdin)) &&
strncmp(s, word, len));
fputs(s? s: "Не найдено.\n", stdout);
}
4.35. Напишите программу, которая переключает свой стандартный вывод в новый файл
имяФайла каждый раз, когда во входном потоке встречается строка вида
>>>>>>имяФайла
Ответ:
#include <stdio.h>
char line[512];
main(){ FILE *fp = fopen("00", "w");
while(gets(line) != NULL)
if( !strncmp(line, ">>>", 3)){
if( freopen(line+3, "a", fp) == NULL){
fprintf(stderr, "Can't write to '%s'\n", line+3);
fp = fopen("00", "a");
}
} else fprintf(fp, "%s\n", line);
}
4.36. Библиотека буферизованного обмена stdio содержит функции, подобные некоторым
системным вызовам. Вот функции - аналоги read и write:
Стандартная функция fread из библиотеки стандартных функций Си предназначена для
чтения нетекстовой (как правило) информации из файла:
____________________
и возвращает код ответа этой программы. Функция popen (pipe open) также запускает
интерпретатор команд, при этом перенаправив его стандартный вывод в трубу (pipe).
Другой конец этой трубы можно читать через канал fp, т.е. можно прочесть в свою прог-
рамму выдачу запущенной команды.
____________________
[*] dup2 читается как "dup to", в английском жаргоне принято обозначать предлог "to"
цифрой 2, поскольку слова "to" и "two" произносятся одинаково: "ту". "From me 2
You". Также 4 читается как "for".
А. Богатырев, 1992-95 - 160 - Си в UNIX
int fread(addr, size, count, fp)
register char *addr; unsigned size, count; FILE *fp;
{ register c; unsigned ndone=0, sz;
if(size)
for( ; ndone < count ; ndone++){
sz = size;
do{ if((c = getc(fp)) >= 0 )
*addr++ = c;
else return ndone;
}while( --sz );
}
return ndone;
}
Заметьте, что count - это не количество БАЙТ (как в read), а количество ШТУК размером
size байт. Функция выдает число целиком прочитанных ею ШТУК. Существует аналогичная
функция fwrite для записи в файл. Пример:
#include <stdio.h>
#define MAXPTS 200
#define N 127
char filename[] = "pts.dat";
struct point { int x,y; } pts[MAXPTS], pp= { -1, -2};
main(){
int n, i;
FILE *fp = fopen(filename, "w");
for(i=0; i < N; i++) /* генерация точек */
pts[i].x = i, pts[i].y = i * i;
/* запись массива из N точек в файл */
fwrite((char *)pts, sizeof(struct point), N, fp);
fwrite((char *)&pp, sizeof pp, 1, fp);
fp = freopen(filename, "r", fp);
/* или fclose(fp); fp=fopen(filename, "r"); */
/* чтение точек из файла в массив */
n = fread(pts, sizeof pts[0], MAXPTS, fp);
for(i=0; i < n; i++)
printf("Точка #%d(%d,%d)\n",i,pts[i].x,pts[i].y);
}
Файлы, созданные fwrite, не переносимы на машины другого типа, поскольку в них хра-
нится не текст, а двоичные данные в формате, используемом данным процессором. Такой
файл не может быть понят человеком - он не содержит изображений данных в виде текста,
а содержит "сырые" байты. Поэтому чаще пользуются функциями работы с текстовыми фай-
лами: fprintf, fscanf, fputs, fgets. Данные, хранимые в виде текста, имеют еще одно
преимущество помимо переносимости: их легко при нужде подправить текстовым редакто-
ром. Зато они занимают больше места!
Аналогом системного вызова lseek служит функция fseek:
fseek(fp, offset, whence);
Она полностью аналогична lseek, за исключением возвращаемого ею значения. Она НЕ
возвращает новую позицию указателя чтения/записи! Чтобы узнать эту позицию применя-
ется специальная функция
long ftell(fp);
Она вносит поправку на положение указателя в буфере канала fp. fseek сбрасывает флаг
"был достигнут конец файла", который проверяется макросом feof(fp);
А. Богатырев, 1992-95 - 161 - Си в UNIX
4.37. Найдите ошибку в программе (программа распечатывает корневой каталог в "ста-
ром" формате каталогов - с фиксированной длиной имен):
#include <stdio.h>
#include <sys/types.h>
#include <sys/dir.h>
main(){
FILE *fp;
struct direct d;
char buf[DIRSIZ+1]; buf[DIRSIZ] = '\0';
fp = fopen( '/', "r" );
while( fread( &d, sizeof d, 1, fp) == 1 ){
if( !d.d_ino ) continue; /* файл стерт */
strncpy( buf, d.d_name, DIRSIZ);
printf( "%s\n", buf );
}
fclose(fp);
}
Указание: смотри в fopen(). Внимательнее к строкам и символам! '/' и "/" - это
совершенно разные вещи (хотя синтаксической ошибки нет!).
Переделайте эту программу, чтобы название каталога поступало из аргументов main
(а если название не задано - используйте текущий каталог ".").
4.38. Функциями
fputs( строка, fp);
printf( формат, ...);
fprintf(fp, формат, ...);
невозможно вывести строку формат, содержащую в середине байт '\0', поскольку он слу-
жит для них признаком конца строки. Однако такой байт может понадобиться в файле,
если мы формируем некоторые нетекстовые данные, например управляющую последователь-
ность переключения шрифтов для принтера. Как быть? Есть много вариантов решения.
Пусть мы хотим выдать в канал fp последовательность из 4х байт "\033e\0\5". Мы можем
сделать это посимвольно:
putc('\033',fp); putc('e', fp);
putc('\000',fp); putc('\005',fp);
(можно просто в цикле), либо использовать один из способов:
fprintf( fp, "\033e%c\5", '\0');
write ( fileno(fp), "\033e\0\5", 4 );
fwrite ( "\033e\0\5", sizeof(char), 4, fp);
где 4 - количество выводимых байтов.
4.39. Напишите функции для "быстрого доступа" к строкам файла. Идея такова: сначала
прочитать весь файл от начала до конца и смещения начал строк (адреса по файлу)
запомнить в массив чисел типа long (точнее, off_t), используя функции fgets() и
ftell(). Для быстрого чтения n-ой строки используйте функции fseek() и fgets().
#include <stdio.h>
#define MAXLINES 2000 /* Максим. число строк в файле*/
FILE *fp; /* Указатель на файл */
int nlines; /* Число строк в файле */
long offsets[MAXLINES];/* Адреса начал строк */
extern long ftell();/*Выдает смещение от начала файла*/
А. Богатырев, 1992-95 - 162 - Си в UNIX
char buffer[256]; /* Буфер для чтения строк */
/* Разметка массива адресов начал строк */
void getSeeks(){
int c;
offsets[0] =0L;
while((c = getc(fp)) != EOF)
if(c =='\n') /* Конец строки - начало новой */
offsets[++nlines] = ftell(fp);
/* Если последняя строка файла не имеет \n на конце, */
/* но не пуста, то ее все равно надо посчитать */
if(ftell(fp) != offsets[nlines])
nlines++;
printf( "%d строк в файле\n", nlines);
}
char *getLine(n){ /* Прочесть строку номер n */
fseek(fp, offsets[n], 0);
return fgets(buffer, sizeof buffer, fp);
}
void main(){ /* печать файла задом-наперед */
int i;
fp = fopen("INPUT", "r"); getSeeks();
for( i=nlines-1; i>=0; --i)
printf( "%3d:%s", i, getLine(i));
}
4.40. Что будет выдано на экран в результате выполнения программы?
#include <stdio.h>
main(){
printf( "Hello, " );
printf( "sunny " );
write( 1, "world", 5 );
}
Ответ: очень хочется ответить, что будет напечатано "Hello, sunny world", поскольку
printf выводит в канал stdout, связанный с дескриптором 1, а дескриптор 1 связан по-
умолчанию с терминалом. Увы, эта догадка верна лишь отчасти! Будет напечатано
"worldHello, sunny ". Это происходит потому, что вывод при помощи функции printf
буферизован, а при помощи сисвызова write - нет. printf помещает строку сначала в
буфер канала stdout, затем write выдает свое сообщение непосредственно на экран,
затем по окончании программы буфер выталкивается на экран.
Чтобы получить правильный эффект, следует перед write() написать вызов явного
выталкивания буфера канала stdout:
fflush( stdout );
Еще одно возможное решение - отмена буферизации канала stdout: перед первым printf
можно написать
setbuf(stdout, NULL);
Имейте в виду, что канал вывода сообщений об ошибках stderr не буферизован исходно,
поэтому выдаваемые в него сообщения печатаются немедленно.
Мораль: надо быть очень осторожным при смешанном использовании буферизованного и
небуферизованного обмена.
А. Богатырев, 1992-95 - 163 - Си в UNIX
Некоторые каналы буферизуются так, что буфер выталкивается не только при запол-
нении, но и при поступлении символа '\n' ("построчная буферизация"). Канал stdout
именно таков:
printf("Hello\n");
печатается сразу (т.к. printf выводит в stdout и есть '\n'). Включить такой режим
буферизации можно так:
setlinebuf(fp); или в других версиях
setvbuf(fp, NULL, _IOLBF, BUFSIZ);
Учтите, что любое изменение способа буферизации должно быть сделано ДО первого обра-
щения к каналу!
4.41. Напишите программу, выдающую три звуковых сигнала. Гудок на терминале вызыва-
ется выдачей символа '\7' ('\a' по стандарту ANSI). Чтобы гудки звучали раздельно,
надо делать паузу после каждого из них. (Учтите, что вывод при помощи printf() и
putchar() буферизован, поэтому после выдачи каждого гудка (в буфер) надо вызывать
функцию fflush() для сброса буфера).
Ответ:
Способ 1:
register i;
for(i=0; i<3; i++){
putchar( '\7' ); fflush(stdout);
sleep(1); /* пауза 1 сек. */
}
Способ 2:
register i;
for(i=0; i<3; i++){
write(1, "\7", 1 );
sleep(1);
}
4.42. Почему задержка не ощущается?
printf( "Пауза...");
sleep ( 5 ); /* ждем 5 сек. */
printf( "продолжаем\n" );
Ответ: из-за буферизации канала stdout. Первая фраза попадает в буфер и, если он не
заполнился, не выдается на экран. Дальше программа "молчаливо" ждет 5 секунд. Обе
фразы будут выданы уже после задержки! Чтобы первый printf() выдал свою фразу ДО
задержки, следует перед функцией sleep() вставить вызов fflush(stdout) для явного
выталкивания буфера. Замечание: канал stderr не буферизован, поэтому проблему можно
решить и так:
fprintf( stderr, "Пауза..." );
4.43. Еще один пример про буферизацию. Почему программа печатает EOF?
#include <stdio.h>
FILE *fwr, *frd;
char b[40], *s; int n = 1917;
main(){
fwr = fopen( "aFile", "w" );
А. Богатырев, 1992-95 - 164 - Си в UNIX
frd = fopen( "aFile", "r" );
fprintf( fwr, "%d: Hello, dude!", n);
s = fgets( b, sizeof b, frd );
printf( "%s\n", s ? s : "EOF" );
}
Ответ: потому что к моменту чтения буфер канала fwr еще не вытолкнут в файл: файл
пуст! Надо вставить
fflush(fwr);
после fprintf(). Вот еще подобный случай:
FILE *fp = fopen("users", "w");
... fprintf(fp, ...); ...
system("sort users | uniq > 00; mv 00 users");
К моменту вызова команды сортировки буфер канала fp (точнее, последний из накопленных
за время работы буферов) может быть еще не вытолкнут в файл. Следует либо закрыть
файл fclose(fp) непосредственно перед вызовом system, либо вставить туда же
fflush(fp);
4.44. В UNIX многие внешние устройства (практически все!) с точки зрения программ
являются просто файлами. Файлы-устройства имеют имена, но не занимают места на диске
(не имеют блоков). Зато им соответствуют специальные программы-драйверы в ядре. При
открытии такого файла-устройства мы на самом деле инициализируем драйвер этого уст-
ройства, и в дальнейшем он выполняет наши запросы read, write, lseek аппаратно-
зависимым образом. Для операций, специфичных для данного устройства, предусмотрен
сисвызов ioctl (input/output control):
ioctl(fd, РОД_РАБОТЫ, аргумент);
где аргумент часто бывает адресом структуры, содержащей пакет аргументов, а
РОД_РАБОТЫ - одно из целых чисел, специфичных для данного устройства (для каждого
устр-ва есть свой собственный список допустимых операций). Обычно РОД_РАБОТЫ имеет
некоторое мнемоническое обозначение.
В качестве примера приведем операцию TCGETA, применимую только к терминалам и
узнающую текущие моды драйвера терминала (см. главу "Экранные библиотеки"). То, что
эта операция неприменима к другим устройствам и к обычным файлам (не устройствам),
позволяет нам использовать ее для проверки - является ли открытый файл терминалом
(или клавиатурой):
#include <termio.h>
int isatty(fd){ struct termio tt;
return ioctl(fd, TCGETA, &tt) < 0 ? 0 : 1;
}
main(){
printf("%s\n", isatty(0 /* STDIN */)? "term":"no"); }
Функция isatty является стандартной функцией[*].
Есть "псевдоустройства", которые представляют собой драйверы логических уст-
ройств, не связанных напрямую с аппаратурой, либо связанных лишь косвенно. Примером
такого устройства является псевдотерминал (см. пример в приложении). Наиболее упот-
ребительны два псевдоустройства:
/dev/null
Это устройство, представляющее собой "черную дыру". Чтение из него немедленно
выдает признак конца файла: read(...)==0; а записываемая в него информация нигде
не сохраняется (пропадает). Этот файл используется, например, в том случае,
когда мы хотим проигнорировать вывод какой-либо программы (сообщения об ошибках,
трассировку), нигде его не сохраняя. Тогда мы просто перенаправляем ее вывод в
/dev/null:
А. Богатырев, 1992-95 - 165 - Си в UNIX
$ a.out > /dev/null &
Еще один пример использования:
$ cp /dev/hd00 /dev/null
Содержимое всего винчестера копируется "в никуда". При этом, если на диске есть
сбойные блоки - система выдает на консоль сообщения об ошибках чтения. Так мы
можем быстро выяснить, есть ли на диске плохие блоки.
/dev/tty
Открытие файла с таким именем в действительности открывает для нас управляющий
терминал, на котором запущена данная программа; даже если ее ввод и вывод были
перенаправлены в какие-то другие файлы[**]. Поэтому, если мы хотим выдать сообще-
ние, которое должно появиться именно на экране, мы должны поступать так:
#include <stdio.h>
void message(char *s){
FILE *fptty = fopen("/dev/tty", "w");
fprintf(fptty, "%s\n", s);
fclose (fptty);
}
main(){ message("Tear down the wall!"); }
Это устройство доступно и для записи (на экран) и для чтения (с клавиатуры).
Файлы устройств нечувствительны к флагу открытия O_TRUNC - он не имеет для них смысла
и просто игнорируется. Поэтому невозможно случайно уничтожить файл-устройство (к при-
меру /dev/tty) вызовом
fd=creat("/dev/tty", 0644);
Файлы-устройства создаются вызовом mknod, а уничтожаются обычным unlink-ом. Более
подробно про это - в главе "Взаимодействие с UNIX".
4.45. Эмуляция основ библиотеки STDIO, по мотивам 4.2 BSD.
#include <fcntl.h>
#define BUFSIZ 512 /* стандартный размер буфера */
#define _NFILE 20
#define EOF (-1) /* признак конца файла */
#define NULL ((char *) 0)
#define IOREAD 0x0001 /* для чтения */
#define IOWRT 0x0002 /* для записи */
#define IORW 0x0004 /* для чтения и записи */
#define IONBF 0x0008 /* не буферизован */
#define IOTTY 0x0010 /* вывод на терминал */
#define IOALLOC 0x0020 /* выделен буфер malloc-ом */
#define IOEOF 0x0040 /* достигнут конец файла */
#define IOERR 0x0080 /* ошибка чтения/записи */
____________________
[*] Заметим еще, что если дескриптор fd связан с терминалом, то можно узнать полное
имя этого устройства вызовом стандартной функции
extern char *ttyname();
char *tname = ttyname(fd);
Она выдаст строку, подобную "/dev/tty01". Если fd не связан с терминалом - она вернет
А. Богатырев, 1992-95 - 166 - Си в UNIX
extern char *malloc(); extern long lseek();
typedef unsigned char uchar;
uchar sibuf[BUFSIZ], sobuf[BUFSIZ];
typedef struct _iobuf {
int cnt; /* счетчик */
uchar *ptr, *base; /* указатель в буфер и на его начало */
int bufsiz, flag, file; /* размер буфера, флаги, дескриптор */
} FILE;
FILE iob[_NFILE] = {
{ 0, NULL, NULL, 0, IOREAD, 0 },
{ 0, NULL, NULL, 0, IOWRT|IOTTY, 1 },
{ 0, NULL, NULL, 0, IOWRT|IONBF, 2 },
};
#define stdin (&iob[0])
#define stdout (&iob[1])
#define stderr (&iob[2])
#define putchar(c) putc((c), stdout)
#define getchar() getc(stdin)
#define fileno(fp) ((fp)->file)
#define feof(fp) (((fp)->flag & IOEOF) != 0)
#define ferror(fp) (((fp)->flag & IOERR) != 0)
#define clearerr(fp) ((void) ((fp)->flag &= ~(IOERR | IOEOF)))
#define getc(fp) (--(fp)->cnt < 0 ? \
filbuf(fp) : (int) *(fp)->ptr++)
#define putc(x, fp) (--(fp)->cnt < 0 ? \
flsbuf((uchar) (x), (fp)) : \
(int) (*(fp)->ptr++ = (uchar) (x)))
int fputc(int c, FILE *fp){ return putc(c, fp); }
int fgetc( FILE *fp){ return getc(fp); }
____________________
NULL.
____________________
[**] Ссылка на управляющий терминал процесса хранится в u-area каждого процесса:
u_ttyp, u_ttyd, поэтому ядро в состоянии определить какой настоящий терминал следует
открыть для вас. Если разные процессы открывают /dev/tty, они могут открыть в итоге
разные терминалы, т.е. одно имя приводит к разным устройствам! Смотри главу про
UNIX.
А. Богатырев, 1992-95 - 167 - Си в UNIX
/* Открытие файла */
FILE *fopen(char *name, char *how){
register FILE *fp; register i, rw;
for(fp = iob, i=0; i < _NFILE; i++, fp++)
if(fp->flag == 0) goto found;
return NULL; /* нет свободного слота */
found:
rw = how[1] == '+';
if(*how == 'r'){
if((fp->file = open(name, rw ? O_RDWR:O_RDONLY)) < 0)
return NULL;
fp->flag = IOREAD;
} else {
if((fp->file = open(name, (rw ? O_RDWR:O_WRONLY)| O_CREAT |
(*how == 'a' ? O_APPEND : O_TRUNC), 0666 )) < 0)
return NULL;
fp->flag = IOWRT;
}
if(rw) fp->flag = IORW;
fp->bufsiz = fp->cnt = 0; fp->base = fp->ptr = NULL;
return fp;
}
/* Принудительный сброс буфера */
void fflush(FILE *fp){
uchar *base; int full= 0;
if((fp->flag & (IONBF|IOWRT)) == IOWRT &&
(base = fp->base) != NULL && (full=fp->ptr - base) > 0){
fp->ptr = base; fp->cnt = fp->bufsiz;
if(write(fileno(fp), base, full) != full)
fp->flag |= IOERR;
}
}
/* Закрытие файла */
void fclose(FILE *fp){
if((fp->flag & (IOREAD|IOWRT|IORW)) == 0 ) return;
fflush(fp);
close(fileno(fp));
if(fp->flag & IOALLOC) free(fp->base);
fp->base = fp->ptr = NULL;
fp->cnt = fp->bufsiz = fp->flag = 0; fp->file = (-1);
}
/* Закрытие файлов при exit()-е */
void _cleanup(){
register i;
for(i=0; i < _NFILE; i++)
fclose(iob + i);
}
/* Завершить текущий процесс */
void exit(uchar code){
_cleanup();
_exit(code); /* Собственно системный вызов */
}
А. Богатырев, 1992-95 - 168 - Си в UNIX
/* Прочесть очередной буфер из файла */
int filbuf(FILE *fp){
static uchar smallbuf[_NFILE];
if(fp->flag & IORW){
if(fp->flag & IOWRT){ fflush(fp); fp->flag &= ~IOWRT; }
fp->flag |= IOREAD; /* операция чтения */
}
if((fp->flag & IOREAD) == 0 || feof(fp)) return EOF;
while( fp->base == NULL ) /* отвести буфер */
if( fp->flag & IONBF ){ /* небуферизованный */
fp->base = &smallbuf[fileno(fp)];
fp->bufsiz = sizeof(uchar);
} else if( fp == stdin ){ /* статический буфер */
fp->base = sibuf;
fp->bufsiz = sizeof(sibuf);
} else if((fp->base = malloc(fp->bufsiz = BUFSIZ)) == NULL)
fp->flag |= IONBF; /* не будем буферизовать */
else fp->flag |= IOALLOC; /* буфер выделен */
if( fp == stdin && (stdout->flag & IOTTY)) fflush(stdout);
fp->ptr = fp->base; /* сбросить на начало буфера */
if((fp->cnt = read(fileno(fp), fp->base, fp->bufsiz)) == 0 ){
fp->flag |= IOEOF; if(fp->flag & IORW) fp->flag &= ~IOREAD;
return EOF;
} else if( fp->cnt < 0 ){
fp->flag |= IOERR; fp->cnt = 0; return EOF;
}
return getc(fp);
}
А. Богатырев, 1992-95 - 169 - Си в UNIX
/* Вытолкнуть очередной буфер в файл */
int flsbuf(int c, FILE *fp){
uchar *base; int full, cret = c;
if( fp->flag & IORW ){
fp->flag &= ~(IOEOF|IOREAD);
fp->flag |= IOWRT; /* операция записи */
}
if((fp->flag & IOWRT) == 0) return EOF;
tryAgain:
if(fp->flag & IONBF){ /* не буферизован */
if(write(fileno(fp), &c, 1) != 1)
{ fp->flag |= IOERR; cret=EOF; }
fp->cnt = 0;
} else { /* канал буферизован */
if((base = fp->base) == NULL){ /* буфера еще нет */
if(fp == stdout){
if(isatty(fileno(stdout))) fp->flag |= IOTTY;
else fp->flag &= ~IOTTY;
fp->base = fp->ptr = sobuf; /* статический буфер */
fp->bufsiz = sizeof(sobuf);
goto tryAgain;
}
if((base = fp->base = malloc(fp->bufsiz = BUFSIZ))== NULL){
fp->bufsiz = 0; fp->flag |= IONBF; goto tryAgain;
} else fp->flag |= IOALLOC;
} else if ((full = fp->ptr - base) > 0)
if(write(fileno(fp), fp->ptr = base, full) != full)
{ fp->flag |= IOERR; cret = EOF; }
fp->cnt = fp->bufsiz - 1;
*base++ = c;
fp->ptr = base;
}
return cret;
}
/* Вернуть символ в буфер */
int ungetc(int c, FILE *fp){
if(c == EOF || fp->flag & IONBF || fp->base == NULL) return EOF;
if((fp->flag & IOREAD)==0 || fp->ptr <= fp->base)
if(fp->ptr == fp->base && fp->cnt == 0) fp->ptr++;
else return EOF;
fp->cnt++;
return(* --fp->ptr = c);
}
/* Изменить размер буфера */
void setbuffer(FILE *fp, uchar *buf, int size){
fflush(fp);
if(fp->base && (fp->flag & IOALLOC)) free(fp->base);
fp->flag &= ~(IOALLOC|IONBF);
if((fp->base = fp->ptr = buf) == NULL){
fp->flag |= IONBF; fp->bufsiz = 0;
} else fp->bufsiz = size;
fp->cnt = 0;
}
А. Богатырев, 1992-95 - 170 - Си в UNIX
/* "Перемотать" файл в начало */
void rewind(FILE *fp){
fflush(fp);
lseek(fileno(fp), 0L, 0);
fp->cnt = 0; fp->ptr = fp->base;
clearerr(fp);
if(fp->flag & IORW) fp->flag &= ~(IOREAD|IOWRT);
}
/* Позиционирование указателя чтения/записи */
#ifdef COMMENT
base ptr случай IOREAD
| |<----cnt---->|
0L |б у |ф е р |
|=======######@@@@@@@@@@@@@@======== файл file
| |<-p->|<-dl-->|
|<----pos---->| | |
|<----offset(new)-->| |
|<----RWptr---------------->|
где pos = RWptr - cnt; // указатель с поправкой
offset = pos + p = RWptr - cnt + p = lseek(file,0L,1) - cnt + p
отсюда: (для SEEK_SET)
p = offset+cnt-lseek(file,0L,1);
или (для SEEK_CUR) dl = RWptr - offset = p - cnt
lseek(file, dl, 1);
Условие, что указатель можно сдвинуть просто в буфере:
if( cnt > 0 && p <= cnt && base <= ptr + p ){
ptr += p; cnt -= p; }
#endif /*COMMENT*/
А. Богатырев, 1992-95 - 171 - Си в UNIX
int fseek(FILE *fp, long offset, int whence){
register resync, c; long p = (-1);
clearerr(fp);
if( fp->flag & (IOWRT|IORW)){
fflush(fp);
if(fp->flag & IORW){
fp->cnt = 0; fp->ptr = fp->base; fp->flag &= ~IOWRT;
}
p = lseek(fileno(fp), offset, whence);
} else if( fp->flag & IOREAD ){
if(whence < 2 && fp->base && !(fp->flag & IONBF)){
c = fp->cnt; p = offset;
if(whence == 0) /* SEEK_SET */
p += c - lseek(fileno(fp), 0L, 1);
else offset -= c;
if(!(fp->flag & IORW) &&
c > 0 && p <= c && p >= fp->base - fp->ptr
){ fp->ptr += (int) p; fp->cnt -= (int) p;
return 0; /* done */
}
resync = offset & 01;
} else resync = 0;
if(fp->flag & IORW){
fp->ptr = fp->base; fp->flag &= ~IOREAD; resync = 0;
}
p = lseek(fileno(fp), offset-resync, whence);
fp->cnt = 0; /* вынудить filbuf(); */
if(resync) getc(fp);
}
return (p== -1 ? -1 : 0);
}
/* Узнать текущую позицию указателя */
long ftell(FILE *fp){
long tres; register adjust;
if(fp->cnt < 0) fp->cnt = 0;
if(fp->flag & IOREAD) adjust = -(fp->cnt);
else if(fp->flag & (IOWRT|IORW)){ adjust = 0;
if(fp->flag & IOWRT &&
fp->base && !(fp->flag & IONBF)) /* буферизован */
adjust = fp->ptr - fp->base;
} else return (-1L);
if((tres = lseek(fileno(fp), 0L, 1)) < 0) return tres;
return (tres + adjust);
}