В UNIX и в MS DOS файлы не имеют предопределенной структуры и представляют собой просто линейные массивы байт. Если вы хотите задать некоторую структуру хранимой информации - вы должны позаботиться об этом в своей программе сами.
Файлы отличаются от обычных массивов тем, что
Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и простых файлов. Об этом и о системе именования файлов прочитайте в документации по UNIX.
Для работы с каким-либо файлом наша программа должна открыть этот файл - установить связь между именем файла и некоторой переменной в программе. При открытии
файла в ядре операционной системы выделяется "связующая" структура file "открытый файл", содержащая:
f_offset: указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как
RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи;
f_flag: режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги;
f_inode: расположение файла на диске (в UNIX - в виде ссылки на I-узел файла*);
и кое-что еще.
У каждого процесса имеется таблица открытых им файлов - это массив ссылок на упомянутые "связующие" структуры**. При открытии файла в этой таблице ищется
- длина файла 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. В нее, в частности, входят таблица открытых процессом файлов, свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого "дескриптора файла".
При закрытии файла связная структура в ядре уничтожается, ячейка в таблице считается свободной, т.е. связь программы и файла разрывается.
Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных программах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь: несколько или один процессов могут открыть один и тот же файл одновременно несколько раз. При этом будет создано несколько "связующих" структур (по одной для каждого открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуация, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание вызова 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;сигнализирующее об ошибке. В этом случае файл надо создать:
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, коды_доступа);
Существует также флаг
*** - Заметим, что на самом деле коды доступа у нового файла будут равны
Все это относится и к созданию каталогов вызовом mkdir.
int unlink(char имя_файла[]);У каждой программы по умолчанию открыты три первых дескриптора, обычно связанные
0 - с клавиатурой (для чтения) 1 - с дисплеем (выдача результатов) 2 - с дисплеем (выдача сообщений об ошибках)Если при вызове close(fd) дескриптор fd не соответствует открытому файлу (не был открыт) - ничего не происходит.
Часто используется такая метафора: если представлять себе файлы как книжки (только чтение) и блокноты (чтение и запись), стоящие на полке, то открытие файла это выбор блокнота по заглавию на его обложке и открытие обложки (на первой странице). Теперь можно читать записи, дописывать, вычеркивать и править записи в середине, листать книжку! Страницы можно сопоставить блокам файла (см. ниже), а "полку" с книжками - каталогу.
Читайте и записывайте файлы большими кусками, кратными 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);Обратите внимание на такие моменты:
RWptr += прочитанное_или_записанное_число_байт;При открытии файла указатель стоит на начале файла: RWptr=0. При записи файл если надо автоматически увеличивает свой размер. При чтении - если мы достигнем конца файла, то read будет возвращать "прочитано 0 байт" (т.е. при чтении указатель чтения не может стать больше размера файла).
int n = read (int fd, char *адрес, unsigned сколькоБайт); int n = write(int fd, char *адрес, unsigned сколькоБайт);Приведем упрощенные схемы логики этих сисвызовов, когда они работают с обычным дисковым файлом (в UNIX устройства тоже выглядят для программ как файлы, но иногда с особыми свойствами):
если( ФАЙЛ[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;
если( ФАЙЛ[fd] не открыт на чтение) то вернуть (-1); если( RWptr >= длина_файла ) то вернуть 0; m = MIN( n, длина_файла - RWptr ); скопировать байты из файла в память процесса addr[ 0..m-1 ] = ФАЙЛ[fd][ RWptr..RWptr+m-1 ]; RWptr += m; вернуть m;
#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 - функция с переменным числом аргументов заранее не определенных типов). Приходится полагаться исключительно на собственную внимательность!
Как по дескриптору файла узнать, открыт он на чтение, запись, чтение и запись одновременно? Вот два варианта решения:
#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++ ){ s = typeOfOpen(i); p = type2OfOpen(i); printf("%d:%s %s\n", i, s? s: "closed", p); } }
Константа NOFILE означает максимальное число одновременно открытых файлов для одного процесса (это размер таблицы открытых процессом файлов, таблицы дескрипторов). Изучите описание системного вызова fcntl (file control).
Напишите функцию rename() для переименования файла. Указание: используйте системные вызовы link() и unlink(). Ответ:
rename( from, to ) char *from, /* старое имя */ *to; /* новое имя */ { unlink( to ); /* удалить файл to */ if( link( from, to ) < 0 ) /* связать */ return (-1); unlink( from ); /* стереть старое имя */ return 0; /* OK */ }
Каталог же, содержащий подкаталоги, имеет некоторое имя в своем родительском каталоге, имя "." в себе самом, и по одному имени ".." в каждом из своих подкаталогов.
Этот вызов будет неудачен, если файл новое_имя уже существует; а также если мы попытаемся создать альтернативное имя в другой файловой системе. Вызов
int fd; close(creat("/tmp/xyz", 0644)); /*Создать пустой файл*/ fd = open("/tmp/xyz", O_RDWR); unlink("/tmp/xyz"); ... close(fd);Первый оператор создает пустой файл. Затем мы открываем файл и уничтожаем его единственное имя. Но поскольку есть программа, открывшая этот файл, он не удаляется немедленно! Программа далее работает с безымянным файлом при помощи дескриптора fd. Как только файл закрывается - он будет уничтожен системой (как не имеющий имен). Такой трюк используется для создания временных рабочих файлов.
Файл можно удалить из каталога только в том случае, если данный каталог имеет для вас код доступа "запись". Коды доступа самого файла при удалении не играют роли.
В современных версиях UNIX есть системный вызов rename, который делает то же самое, что и написанная нами одноименная функция.
Существование альтернативных имен у файла позволяет нам решить некоторые проблемы, которые могут возникнуть при использовании чужой программы, от которой нет исходного текста (которую нельзя поправить). Пусть программа выдает некоторую информацию в файл 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или программно:
/* Эта программа компилируется в 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.
Еще раз:
Напишите программу, которая находит в файле символ @ и выдает файл с этого места дважды. Указание: для запоминания позиции в файле используйте вызов lseek() позиционирование указателя чтения/записи:
long offset, lseek(); ... /* Узнать текущую позицию чтения/записи: * сдвиг на 0 от текущей позиции. lseek вернет новую * позицию указателя (в байтах от начала файла). */ offset = lseek(fd, 0L, 1); /* ftell(fp) */А для возврата в эту точку:
lseek(fd, offset, 0); /* fseek(fp, offset, 0) */По поводу lseek надо помнить такие вещи:
0 от начала файла RWptr = offset; 1 от текущей позиции RWptr += offset; 2 от конца файла RWptr = длина_файла + offset;Эти значения whence можно обозначать именами:
#include <stdio.h> 0 это SEEK_SET 1 это SEEK_CUR 2 это SEEK_END
/* это стандартная функция */ int isapipe(int fd){ extern errno; return (lseek(fd, 0L, SEEK_CUR) < 0 && errno == ESPIPE); }выдает "истину", если fd - дескриптор "трубы"(pipe).
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 - нет! Вот кое-что про механику выделения блоков:
В нашем примере: при создании файла его размер 0, и ему выделено 0 блоков. При первой записи файлу будет выделен один блок (логический блок номер 0 для файла) и в его начало запишется "begin". Длина файла станет равна 5 (остаток блока - 1019 байт - не используется и файлу логически не принадлежит!). Затем lseek поставит указатель записи далеко за конец файла и write запишет в 1000-ый блок слово "end". 1000-ый блок будет выделен на диске. В этот момент у файла "возникнут" и все промежуточные блоки 1..999. Однако они будут только "числиться за файлом", но на диске отведены не будут (в таблице блоков файла это обозначается адресом 0)! При чтении из них будут читаться байты '\0'. Это так называемая "дырка" в файле. Файл имеет размер 1024003 байта, но на диске занимает всего 2 блока (на самом деле чуть больше, т.к. часть таблицы блоков файла тоже находится в специальных блоках файла). Блок из "дырки" станет реальным, если в него что-нибудь записать.
Будьте готовы к тому, что "размер файла" (который, кстати, можно узнать системным вызовом stat) - это в UNIX не то же самое, что "место, занимаемое файлом на диске".
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 - указателю на файл. Это параллельные механизмы (по своим возможностям), хотя второй является просто надстройкой над первым. Тем не менее, лучше их не смешивать.
* I-узел (I-node, индексный узел) - своеобразный "паспорт", который есть у каждого файла (в том числе и каталога). В нем содержатся:
** BSD - семейство UNIX-ов из University of California, Berkley. Berkley Software Distribution.
© Copyright А. Богатырев, 1992-95 www.abyss-group.narod.ru
Си в UNIX
Назад | Содержание | Вперед