WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 | 5 |   ...   | 6 |

«А. Богатырёв, 1992-96 -1- Си в UNIX™ А. Богатырёв Язык Си в системе UNIX ...»

-- [ Страница 3 ] --

Часто используется такая метафора: если представлять себе файлы как книжки (только чтение) и блокноты (чтение и запись), стоящие на полке, то открытие файла - это выбор блокнота по заглавию на его обложке и открытие обложки (на первой странице). Теперь можно читать записи, дописывать, вычеркивать и править записи в середине, листать книжку! Страницы можно сопоставить блокам файла (см. ниже), а "полку" с книжками - каталогу.

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";

† Заметим, что на самом деле коды доступа у нового файла будут равны di_mode = (коды_доступа & ~u_cmask) | IFREG;

(для каталога вместо IFREG будет IFDIR), где маска u_cmask задается системным вызовом umask(u_cmask);

(вызов выдает прежнее значение маски) и в дальнейшем наследуется всеми потомками данного процесса (она хранится в u-area процесса). Эта маска позволяет запретить доступ к определенным операциям для всех создаваемых нами файлов, несмотря на явно заданные коды доступа, например umask(0077);

/* ???------ */ делает значащими только первые 3 бита кодов доступа (для владельца файла). Остальные биты будут рав ны нулю.

Все это относится и к созданию каталогов вызовом mkdir.

А. Богатырёв, 1992-96 - 121 - Си в UNIX™ 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. При записи файл если надо автомати чески увеличивает свой размер. При чтении - если мы достигнем конца файла, то 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. Найдите ошибки в фрагменте программы:

А. Богатырёв, 1992-96 - 122 - Си в UNIX™ #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 - функция с переменным числом аргументов заранее не определенных типов). Приходится полагаться исключительно на собственную внимательность!

4.4. Как по дескриптору файла узнать, открыт он на чтение, запись, чтение и запись одновременно? Вот два варианта решения:

#include #include #include /* там определено NOFILE */ #include 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).

А. Богатырёв, 1992-96 - 123 - Си в UNIX™ 4.5. Напишите функцию rename() для переименования файла. Указание: используйте системные вызовы link() и unlink(). Ответ:

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-96 - 124 - Си в UNIX™ /* Эта программа компилируется в start */ /* и вызывается вместо a.out */ #include 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.

symbolic link - создается вызовом symlink. Это отдельный самостоятельный файл, с собственным I-node. Правда, коды доступа к этому файлу не играют никакой роли;

значимы только коды доступа указуемого файла.

4.7. Напишите программу, которая находит в файле символ @ и выдает файл с этого места дважды. Указа ние: для запоминания позиции в файле используйте вызов lseek() - позиционирование указателя чте ния/записи:

† BSD - семейство UNIX-ов из University of California, Berkley. Berkley Software Distribution.

А. Богатырёв, 1992-96 - 125 - Си в UNIX™ 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 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).

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, где блок может делиться на фраг менты, принадлежащие разным файлам). Операционная система в каждый момент времени знает длину файла с точностью до одного байта и не позволяет нам "заглядывать" в остаток блока, пока при своем "росте" файл не займет эти байты.

А. Богатырёв, 1992-96 - 126 - Си в UNIX™ • Блок на диске физически выделяется лишь после операции записи в этот блок.

В нашем примере: при создании файла его размер 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 - указа телю на файл. Это параллельные механизмы (по своим возможностям), хотя второй является просто над стройкой над первым. Тем не менее, лучше их не смешивать.

4.10. Доступ к диску (чтение/запись) гораздо (на несколько порядков) медленнее, чем доступ к данным в оперативной памяти. Кроме того, если мы читаем или записываем файл при помощи системных вызовов маленькими порциями (по 1-10 символов) char c;

while( read(0, &c, 1))... ;

/* 0 - стандартный ввод */ то мы проигрываем еще в одном: каждый системный вызов - это обращение к ядру операционной системы.

При каждом таком обращении происходит довольно большая дополнительная работа (смотри главу "Взаи модействие с UNIX"). При этом накладные расходы на такое посимвольное чтение файла могут значительно превысить полезную работу.

Еще одной проблемой является то, что системные вызовы работают с файлом как с неструктуриро ванным массивом байт;

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

Для решения этих двух проблем была построена специальная библиотека функций, названная stdio "стандартная библиотека ввода/вывода" (standard input/output library). Она является частью библиотеки /lib/libc.a и представляет собой надстройку над системными вызовами (т.к. в конце концов все ее функции время от времени обращаются к системе, но гораздо реже, чем если использовать сисвызовы непосред ственно). Небезызвестная директива #include включает в нашу программу файл с объявлением форматов данных и констант, используемых этой библиотекой.

Библиотеку stdio можно назвать библиотекой буферизованного обмена, а также библиотекой работы с текстовыми файлами (т.е. имеющими разделение на строки), поскольку для оптимизации обменов с дис ком (для уменьшения числа обращений к нему и тем самым сокращения числа системных вызовов) эта библиотека вводит буферизацию, а также предоставляет несколько функций для работы со строчно организованными файлами.

Связь с файлом в этой модели обмена осуществляется уже не при помощи целого числа - дескрип тора файла (file descriptor), а при помощи адреса "связной" структуры FILE. Указатель на такую структуру условно называют указателем на файл (file pointer)†. Структура FILE содержит в себе:

• дескриптор fd файла для обращения к системным вызовам;

† Это не та "связующая" структура file в ядре, про которую шла речь выше, а ЕЩЕ одна - в памяти са мой программы.

А. Богатырёв, 1992-96 - 127 - Си в UNIX™ • указатель на буфер, размещенный в памяти программы;

• указатель на текущее место в буфере, откуда надо выдать или куда записать очередной символ;

этот указатель продвигается при каждом вызове getc или putc;

• счетчик оставшихся в буфере символов (при чтении) или свободного места (при записи);

• режимы открытия файла (чтение/запись/чтение+запись) и текущее состояние файла. Одно из состояний - при чтении файла был достигнут его конец‡;

• способ буферизации;

Предусмотрено несколько стандартных структур FILE, указатели на которые называются stdin, stdout и stderr и связаны с дескрипторами 0, 1, 2 соответственно (стандартный ввод, стандартный вывод, стандартный вывод ошибок). Напомним, что эти каналы открыты неявно (автоматически) и, если не перенаправлены, свя заны с вводом с клавиатуры и выводом на терминал.

Буфер в оперативной памяти нашей программы создается (функцией malloc) при открытии файла при помощи функции fopen(). После открытия файла все операции обмена с файлом происходят не по байту, а большими порциями размером с буфер - обычно по 512 байт (константа BUFSIZ).

При чтении символа int c;

FILE *fp =... ;

c = getc(fp);

в буфер считывается 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, осуществляющим ввод/вывод низкого уровня.

Системные вызовы далее обозначены жирно, макросы - курсивом.

‡ Проверить это состояние позволяет макрос feof(fp);

он истинен, если конец был достигнут, ложен если еще нет.

†† При выполнении вызова завершения программы exit();

все открытые файлы автоматически закры ваются.

‡‡ Обозначения fd для дескрипторов и fp для указателей на файл прижились и их следует придержи ваться. Если переменная должна иметь более мнемоничное имя - следует писать так: fp_output, fd_input (а не просто fin, fout).

А. Богатырёв, 1992-96 - 128 - Си в UNIX™ Открыть файл, создать буфер:

#include 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-).

Соответствие аргументов fopen и open:

rwmode irwmode ------------------------ "r" O_RDONLY "w" |O_TRUNC O_WRONLY|O_CREAT "r+" O_RDWR "w+" O_RDWR |O_CREAT |O_TRUNC "a" |O_APPEND O_WRONLY|O_CREAT "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 можно узнать дескриптор файла:

А. Богатырёв, 1992-96 - 129 - Си в UNIX™ int fd = fileno(fp);

Это макроопределение просто выдает поле из структуры FILE. Обратно, если мы открыли файл 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 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) 4.13. Известная вам функция printf также является частью библиотеки stdio. Она входит в семейство функций:

FILE *fp;

char bf[256];

fprintf(fp, fmt,... );

fmt,... );

printf( sprintf(bf, fmt,... );

Первая из функций форматирует свои аргументы в соответствии с форматом, заданным строкой fmt (она содержит форматы в виде %-ов) и записывает строку-результат посимвольно (вызывая putc) в файл fp.

Вторая - это всего-навсего fprintf с каналом fp равным stdout. Третяя выдает сформатированную строку не в файл, а записывает ее в массив bf. В конце строки sprintf добавляет нулевой байт '\0' - признак конца.

А. Богатырёв, 1992-96 - 130 - Си в UNIX™ Для чтения данных по формату используются функции семейства fscanf(fp, fmt, /* адреса арг-тов */...);

fmt,... );

scanf( sscanf(bf, fmt,... );

Функции fprintf и fscanf являются наиболее мощным средством работы с текстовыми файлами (содержа щими изображение данных в виде печатных символов).

4.14. Текстовые файлы (имеющие строчную организацию) хранятся на диске как линейные массивы байт.

Для разделения строк в них используется символ '\n'. Так, например, текст стр стрк кнц хранится как массив с т р 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 );

Все нетекстовые файлы в 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 бит). Проверка на конец файла в программе обычно выглядит так:

† Управляющие символы имеют следующие значения:

- '\012' (10) line feed '\n' - '\015' (13) carriage return '\r' - '\011' (9) tab '\t' - '\010' (8) backspace '\b' - '\014' (12) form feed '\f' - '\007' (7) audio bell (alert) '\a' - 0. null byte '\0' А. Богатырёв, 1992-96 - 131 - Си в UNIX™...

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.

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. Пока файл не дочитан, эти функции возвращают свой первый аргумент - адрес буфера, в который была записана очередная строка файла.

Фрагмент для обрубания символа перевода строки может выглядеть еще так:

А. Богатырёв, 1992-96 - 132 - Си в UNIX™ #include #include 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');

} 4.19. Найдите ошибки в программе:

#include 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 #include 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;

А. Богатырёв, 1992-96 - 133 - Си в UNIX™ } 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;

} 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' завершает просмотр. Если файл, который вы хотите просмотреть, не существует - выдавайте сообщение об ошибке.

После выполнения действия программа вновь запрашивает имя файла. Если вы ответите вводом пустой строки (сразу нажмете , то должно использоваться имя файла, введенное на предыдущем шаге. Имя файла, предлагаемое по умолчанию, принято писать в запросе в [] скобках.

Введите имя файла [oldfile.txt]: _ Когда вы научитесь работать с экраном дисплея (см. главу "Экранные библиотеки"), перепишите меню и выдачу сообщений с использованием позиционирования курсора в заданное место экрана и с выделением текста инверсией. Для выбора имени файла предложите меню: отсортированный список имен всех файлов текущего каталога (по поводу получения списка файлов см. главу про взаимодействие с UNIX). Просто для распечатки текущего каталога на экране можно также использовать вызов system("ls -x");

а для считывания каталога в программу† † Функция int system(char *команда);

выполняет команду, записанную в строке команда, вызывая для этого интерпретатор команд /bin/sh -c "команда" и возвращает код ответа этой программы. Функция popen (pipe open) также запускает интерпретатор ко манд, при этом перенаправив его стандартный вывод в трубу (pipe). Другой конец этой трубы можно читать через канал fp, т.е. можно прочесть в свою программу выдачу запущенной команды.

А. Богатырёв, 1992-96 - 134 - Си в UNIX™ FILE *fp = popen("ls *.c", "r");

... fgets(...,fp);

... // в цикле, пока не EOF pclose(fp);

(в этом примере читаются только имена.c файлов).

4.28. Напишите программу удаления n-ой строки из файла;

вставки строки после m-ой. К сожалению, это возможно только путем переписывания всего файла в другое место (без ненужной строки) и последующего его переименования.

4.29. Составьте программу перекодировки текста, набитого в кодировке КОИ-8, в альтернативную коди ровку и наоборот. Для этого следует составить таблицу перекодировки из 256 символов:

c_new=TABLE[c_old];

Для решения обратной задачи используйте стандартную функцию strchr(). Программа читает один файл и создает новый.

4.30. Напишите программу, делящую большой файл на куски заданного размера (не в строках, а в кило байтах). Эта программа может применяться для записи слишком большого файла на дискеты (файл режется на части и записывается на несколько дискет).

#include #include #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);

А. Богатырёв, 1992-96 - 135 - Си в UNIX™ } printf("%s\t%lu байт\n", name, sent);

} while( !done );

exit(0);

} 4.31. Напишите обратную программу, которая склеивает несколько файлов в один. Это аналог команды cat с единственным отличием: результат выдается не в стандартный вывод, а в файл, указанный в строке аргументов последним. Для выдачи в стандартный вывод следует указать имя "-".

#include #include 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 #endif int fdin = open( av[1], O_RDONLY | O_BINARY);

4.32. Каким образом стандартный ввод переключить на ввод из заданного файла, а стандартный вывод - в файл? Как проверить, существует ли файл;

пуст ли он? Как надо открывать файл для дописывания инфор мации в конец существующего файла? Как надо открывать файл, чтобы попеременно записывать и читать тот же файл? Указание: см. fopen, freopen, dup2, stat. Ответ про перенаправления ввода:

способ 1 (библиотечные функции) #include ...

freopen( "имя_файла", "r", stdin );

способ 2 (системные вызовы) #include int fd;

...

fd = open( "имя_файла", O_RDONLY );

dup2 ( fd, 0 );

/* 0 - стандартный ввод */ close( fd );

/* fd больше не нужен - закрыть его, чтоб не занимал место в таблице */ А. Богатырёв, 1992-96 - 136 - Си в UNIX™ способ 3 (системные вызовы) #include 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---##---> файл ## ## old----##---> файл2 old---## файл ## ## 0:до вызова 1:разрыв связи old с файл dup2() (закрытие канала old, если он был открыт) ## ## new----##--*--> файл1 new ## *----> файл ## | ## | 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 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. Напишите программу, распечатывающую первую директиву препроцессора, встретившуюся в файле ввода.

† dup2 читается как "dup to", в английском жаргоне принято обозначать предлог "to" цифрой 2, по скольку слова "to" и "two" произносятся одинаково: "ту". "From me 2 You". Также 4 читается как "for".

А. Богатырёв, 1992-96 - 137 - Си в UNIX™ #include 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 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 из библиотеки стандартных функций Си предназначена для чтения нетек стовой (как правило) информации из файла:

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 #define MAXPTS #define N 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");

*/ А. Богатырёв, 1992-96 - 138 - Си в UNIX™ /* чтение точек из файла в массив */ 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);

4.37. Найдите ошибку в программе (программа распечатывает корневой каталог в "старом" формате ката логов - с фиксированной длиной имен):

#include #include #include 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);

(можно просто в цикле), либо использовать один из способов:

"\033e%c\5", '\0');

fprintf( fp, write ( fileno(fp), "\033e\0\5", 4 );

fwrite ( "\033e\0\5", sizeof(char), 4, fp);

где 4 - количество выводимых байтов.

А. Богатырёв, 1992-96 - 139 - Си в UNIX™ 4.39. Напишите функции для "быстрого доступа" к строкам файла. Идея такова: сначала прочитать весь файл от начала до конца и смещения начал строк (адреса по файлу) запомнить в массив чисел типа long (точнее, off_t), используя функции fgets() и ftell(). Для быстрого чтения n-ой строки используйте функции fseek() и fgets().

#include #define MAXLINES 2000 /* Максим. число строк в файле*/ FILE *fp;

/* Указатель на файл */ int nlines;

/* Число строк в файле */ long offsets[MAXLINES];

/* Адреса начал строк */ extern long ftell();

/*Выдает смещение от начала файла*/ 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 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-96 - 140 - Си в 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 FILE *fwr, *frd;

char b[40], *s;

int n = 1917;

main(){ fwr = fopen( "aFile", "w" );

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) А. Богатырёв, 1992-96 - 141 - Си в UNIX™ непосредственно перед вызовом system, либо вставить туда же fflush(fp);

4.44. В UNIX многие внешние устройства (практически все!) с точки зрения программ являются просто файлами. Файлы-устройства имеют имена, но не занимают места на диске (не имеют блоков). Зато им соответствуют специальные программы-драйверы в ядре. При открытии такого файла-устройства мы на самом деле инициализируем драйвер этого устройства, и в дальнейшем он выполняет наши запросы read, write, lseek аппаратно-зависимым образом. Для операций, специфичных для данного устройства, преду смотрен сисвызов ioctl (input/output control):

ioctl(fd, РОД_РАБОТЫ, аргумент);

где аргумент часто бывает адресом структуры, содержащей пакет аргументов, а РОД_РАБОТЫ - одно из целых чисел, специфичных для данного устройства (для каждого устр-ва есть свой собственный список допустимых операций). Обычно РОД_РАБОТЫ имеет некоторое мнемоническое обозначение.

В качестве примера приведем операцию TCGETA, применимую только к терминалам и узнающую текущие моды драйвера терминала (см. главу "Экранные библиотеки"). То, что эта операция неприменима к другим устройствам и к обычным файлам (не устройствам), позволяет нам использовать ее для проверки является ли открытый файл терминалом (или клавиатурой):

#include 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:

$ a.out > /dev/null & Еще один пример использования:

$ cp /dev/hd00 /dev/null Содержимое всего винчестера копируется "в никуда". При этом, если на диске есть сбойные блоки система выдает на консоль сообщения об ошибках чтения. Так мы можем быстро выяснить, есть ли на диске плохие блоки.

/dev/tty Открытие файла с таким именем в действительности открывает для нас управляющий терминал, на котором запущена данная программа;

даже если ее ввод и вывод были перенаправлены в какие-то другие файлы‡. Поэтому, если мы хотим выдать сообщение, которое должно появиться именно на экране, мы должны поступать так:

† Заметим еще, что если дескриптор fd связан с терминалом, то можно узнать полное имя этого устройства вызовом стандартной функции extern char *ttyname();

char *tname = ttyname(fd);

Она выдаст строку, подобную "/dev/tty01". Если fd не связан с терминалом - она вернет NULL.

‡ Ссылка на управляющий терминал процесса хранится в u-area каждого процесса: u_ttyp, u_ttyd, по этому ядро в состоянии определить какой настоящий терминал следует открыть для вас. Если разные про цессы открывают /dev/tty, они могут открыть в итоге разные терминалы, т.е. одно имя приводит к разным устройствам! Смотри главу про UNIX.

А. Богатырёв, 1992-96 - 142 - Си в UNIX™ #include 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 #define BUFSIZ 512 /* стандартный размер буфера */ #define _NFILE #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 /* ошибка чтения/записи */ 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 }, };

А. Богатырёв, 1992-96 - 143 - Си в UNIX™ #define stdin (&iob[0]) #define stdout (&iob[1]) #define stderr (&iob[2]) #define putc((c), stdout) putchar(c) #define getc(stdin) getchar() #define ((fp)->file) fileno(fp) #define (((fp)->flag & IOEOF) != 0) feof(fp) #define (((fp)->flag & IOERR) != 0) ferror(fp) #define ((void) ((fp)->flag &= ~(IOERR | IOEOF))) clearerr(fp) #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);

} /* Открытие файла */ 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);

} А. Богатырёв, 1992-96 - 144 - Си в UNIX™ /* Закрытие файлов при exit()-е */ void _cleanup(){ register i;

for(i=0;

i < _NFILE;

i++) fclose(iob + i);

} /* Завершить текущий процесс */ void exit(uchar code){ _cleanup();

_exit(code);

/* Собственно системный вызов */ } /* Прочесть очередной буфер из файла */ 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-96 - 145 - Си в 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;

} /* "Перемотать" файл в начало */ 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);

} А. Богатырёв, 1992-96 - 146 - Си в UNIX™ /* Позиционирование указателя чтения/записи */ #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*/ 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);

} А. Богатырёв, 1992-96 - 147 - Си в UNIX™ /* Узнать текущую позицию указателя */ 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);

} А. Богатырёв, 1992-96 - 148 - Си в UNIX™ 5. Структуры данных.

Структуры ("записи") представляют собой агрегаты разнородных данных (полей разного типа);

в отличие от массивов, где все элементы имеют один и тот же тип.

struct { int x, y;

/* два целых поля */ char s[10];

/* и одно - для строки */ } s1;

Структурный тип может иметь имя:

struct XYS { int x, y;

/* два целых поля */ char str[10];

/* и одно - для строки */ };

Здесь мы объявили тип, но не отвели ни одной переменной этого типа (хотя могли бы). Теперь опишем переменную этого типа и указатель на нее:

struct XYS s2, *sptr = &s2;

Доступ к полям структуры производится по имени поля (а не по индексу, как у массивов):

имя_структурной_переменной.имя_поля указатель_на_структуру -> имя_поля то есть не а #define ВЕС 0 struct { int вес, рост;

} x;

#define РОСТ 1 x.рост = 175;

int x[2];

x[РОСТ] = 175;

Например s1.x = 13;

strcpy(s2.str, "Finish");

sptr->y = 27;

Структура может содержать структуры другого типа в качестве полей:

struct XYS_Z { struct XYS xys;

int z;

} a1;

a1.xys.x = 71;

a1.z = 12;

Структура того же самого типа не может содержаться в качестве поля - рекурсивные определения запре щены. Зато нередко используются поля - ссылки на структуры такого же типа (или другого). Это позволяет организовывать списки структур:

struct node { int value;

struct node *next;

};

Очень часто используются массивы структур:

struct XYS array[20];

int i = 5, j;

array[i].x = 12;

j = array[i].x;

Статические структуры можно описывать с инициализацией, перечисляя значения их полей в {} через запя тую:

extern struct node n2;

struct node n1 = { 1, &n2 }, n2 = { 2, &n1 }, n3 = { 3, NULL };

В этом примере n2 описано предварительно для того, чтобы &n2 в строке инициализации n1 было опреде лено.

Структуры одинакового типа можно присваивать целиком (что соответствует присваиванию каждого из полей):

А. Богатырёв, 1992-96 - 149 - Си в UNIX™ struct XYS s1, s2;

...

s2 = s1;

в отличие от массивов, которые присваивать целиком нельзя:

int a[5], b[5];

a = b;

/* ОШИБОЧНО ! */ Пример обращения к полям структуры:

typedef struct _Point { short x, y;

/* координаты точки */ char *s;

/* метка точки */ } Point;

Point p;

Point *pptr;

short *iptr;

struct _Curve { Point points[25];

/* вершины ломанной */ int color;

/* цвет линии */ } aLine[10], *linePtr = & aLine[0];

...

pptr = &p;

/* указатель на структуру p */ p.x = 1;

p.y = 2;

p.s = "Grue";

linePtr->points[2].x = 54;

aLine[5].points[0].y = 17;

Выражение значение ---------+------------+------------+-----------+---------- p.x | pptr->x | (*pptr).x | (&p)->x | ---------+------------+------------+-----------+---------- &p->x | ошибка -----------+----------------+------------------+---------- iptr= &p.x | iptr= &pptr->x | iptr= &(pptr->x) | адрес поля -----------+----------------+--------+---------+---------- *pptr->s | *(pptr->s) | *p.s | p.s[0] | 'G' -----------+----------------+--------+---------+---------- pptr->s[1] | (&p)->s[1] | p.s[1] | 'r' -----------+----------------+------------------+---------- &p->s[1] | ошибка -----------+----------------+------------------+---------- (*pptr).s | pptr->s | p.s | "Grue" -----------+----------------+------------------+---------- *pptr.s | ошибка -----------------------------------------------+---------- Вообще (&p)->field = p.field pptr->field = (*pptr).field Объединения - это агрегаты данных, которые могут хранить в себе значения данных разных типов на одном и том же месте.

struct a{ int x, y;

char *s;

} A;

union b{ int i;

char *s;

struct a aa;

} B;

Структура:

A: | A.x int | Три поля ------------------------ расположены подряд.

| A.y int | Получается как бы ------------------------ "карточка" с графами.

| A.s char * | ----------------------- А у объединений поля расположены "параллельно", на одном месте в памяти.

_ B: | B.i int | B.s char * | B.aa : B.aa.x int | -----------| | struct a : B.aa.y int | ---------------| : B.aa.s char * | |_| Это как бы "ящик" в который можно поместить значение любого типа из перечисленных, но не ВСЕ ВМЕСТЕ ("и то и это", как у структур), а ПО ОЧЕРЕДИ ("или/или"). Размер его достаточно велик, чтоб вместить А. Богатырёв, 1992-96 - 150 - Си в UNIX™ самый большой из перечисленных типов данных.

Мы можем занести в union значение и интерпретировать его как другой тип данных - это иногда используется в машинно-зависимых программах. Вот пример, выясняющий порядок байтов в short числах:

union lb { char s[2];

short i;

} x;

unsigned hi, lo;

x.i = (02 << 8) | 01;

hi = x.s[1];

lo = x.s[0];

printf( "%d %d\n", hi, lo);

или так:

#include union { int i;

unsigned char s[sizeof(int)];

} u;

void main(){ unsigned char *p;

int n;

u.i = 0x12345678;

for(n=0, p=u.s;

n < sizeof(int);

n++, p++){ printf("%02X ", *p);

} putchar('\n');

} или порядок слов в long числах:

union xx { long l;

struct ab { short a;

/* low word */ short b;

/* high word */ } ab;

} c;

main(){ /* На IBM PC 80386 печатает 00020001 */ c.ab.a = 1;

c.ab.b = 2;

printf("%08lx\n", c.l );

} 5.1. Найдите ошибки в описании структурного шаблона:

structure { int arr[12], char string, int *sum } 5.2. Разработайте структурный шаблон, который содержал бы название месяца, трехбуквенную аббревиа туру месяца, количество дней в месяце и номер месяца. Инициализируйте его для невисокосного года.

struct month { char name[10];

/* или char *name;

*/ char abbrev[4];

/* или char *abbrev;

*/ int days;

int num;

};

struct month months[12] = { /* индекс */ {"Январь", "Янв", 31, 1 }, /* 0 */ {"Февраль", "Фев", 28, 2 }, /* 1 */...

{"Декабрь", "Дек", 31, 12}, /* 11 */ }, *mptr = & months[0];

/* или *mptr = months */ А. Богатырёв, 1992-96 - 151 - Си в UNIX™ main(){ struct month *mptr;

printf( "%s\n", mptr[1].name );

printf( "%s %d\n", mptr->name, mptr->num );

} Напишите функцию, сохраняющую массив months в файл;

функцию, считывающую его из файла. Исполь зуйте fprintf и fscanf.

В чем будет разница в функции чтения, когда поле name описано как char name[10] и как char *name?

Ответ: во втором случае для сохранения прочитанной строки надо заказывать память динамически при помощи malloc() и сохранять в ней строку при помощи strcpy(), т.к. память для хранения самой строки в структуре не зарезервирована (а только для указателя на нее).

Найдите ошибку в операторах функции main(). Почему печатается не "Февраль", а какой-то мусор?

Указание: куда указывает указатель mptr, описанный в main()? Ответ: в "неизвестно куда" - это локальная переменная (причем не получившая начального значения - в ней содержится мусор), а не то же самое, что указатель mptr, описанный выше! Уберите описание mptr из main.

Заметим, что для распечатки всех или нескольких полей структуры следует ЯВНО перечислить в printf() все нужные поля и указать форматы, соответствующие типам этих полей. Не существует формата или стандартной функции, позволяющей распечатать все поля сразу (однако такая функция может быть написана вами для конкретного типа структур). Также не существует формата для scanf(), который вводил бы структуру целиком. Вводить можно только по частям - каждое поле отдельно.

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

5.4. Переделайте предыдущую программу таким образом, чтобы она по написанному буквами названию месяца возвращала общее число дней года вплоть до этого месяца. В программе используйте функцию strcmp().

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

5.6. Составьте структуру для учетной картотеки служащего, которая содержала бы следующие сведения:

фамилию, имя, отчество;

год рождения;

домашний адрес;

место работы, должность;

зарплату;

дату посту пления на работу.

5.7. Что печатает программа?

struct man { char name[20];

int salary;

} workers[] = { { "Иванов", 200 }, { "Петров", 180 }, { "Сидоров", 150 } }, *wptr, chief = { "начальник", 550 };

main(){ struct man *ptr, *cptr, save;

ptr = wptr = workers + 1;

cptr = &chief;

save = workers[2];

workers[2] = *wptr;

*wptr = save;

wptr++;

ptr--;

ptr->salary = save.salary;

printf( "%c %s %s %s %s\n%d %d %d %d\n%d %d %c\n", *workers[1].name, workers[2].name, cptr->name, ptr[1].name, save.name, wptr->salary, chief.salary, (*ptr).salary, workers->salary, wptr - ptr, wptr - workers, *ptr->name );

} Ответ:

А. Богатырёв, 1992-96 - 152 - Си в UNIX™ С Петров начальник Сидоров Сидоров 180 550 150 22И 5.8. Разберите следующий пример:

#include struct man{ char *name, town[4];

int salary;

int addr[2];

} men[] = { { "Вася", "Msc", 100, { 12, 7 } }, { "Гриша", "Len", 120, { 6, 51 } }, { "Петя", "Rig", 140, { 23, 84 } },, { NULL, "" -1, { -1, -1 } } };

main(){ struct man *ptr, **ptrptr;

int i;

ptrptr = &ptr;

*ptrptr = &men[1];

/* men+1 */ printf( "%s %d %s %d %c\n", ptr->name, ptr->salary, ptr->town, ptr->addr[1], ptr[1].town[2] );

(*ptrptr)++;

/* копируем *ptr в men[0] */ men[0].name = ptr->name;

/* (char *) #1 */ strcpy( men[0].town, ptr->town );

/* char [] #2 */ men[0].salary = ptr->salary;

/* int #3 */ for( i=0;

i < 2;

i++ ) men[0].addr[i] = ptr->addr[i];

/* массив #4 */ /* распечатываем массив структур */ for(ptr=men;

ptr->name;

ptr++ ) printf( "%s %s %d\n", ptr->name, ptr->town, ptr->addr[0]);

} Обратите внимание на такие моменты:

1) Как производится работа с указателем на указатель (ptrptr).

2) При копировании структур отдельными полями, поля скалярных типов (int, char, long,..., указатели) копируются операцией присваивания (см. строки с пометками #1 и #3). Поля векторных типов (мас сивы) копируются при помощи цикла, поэлементно пересылающего массив (строка #4). Строки (мас сивы букв) пересылаются стандартной функцией strcpy (строка #2). Все это относится не только к полям структур, но и к переменным таких типов. Структуры можно также копировать не по полям, а целиком: men[0]= *ptr;

3) Запись аргументов функции printf() лесенкой позволяет лучше видеть, какому формату соответствует каждый аргумент.

4) При распечатке массива структур мы печатаем не определенное их количество (равное размеру мас сива), а пользуемся указателем NULL в поле name последней структуры как признаком конца мас сива.

5) В поле town мы храним строки из 3х букв, однако выделяем для хранения массив из 4х байт. Это необходимо потому, что строка "Msc" состоит не из 3х, а из 4х байтов: 'M','s','c','\0'.

При работе со структурами и указателями большую помощь могут оказать рисунки. Вот как (например) можно нарисовать данные из этого примера (массив men изображен не весь):

А. Богатырёв, 1992-96 - 153 - Си в UNIX™ --ptr-- --ptrptr- ptr | * |<------|---* | ---|--- --------- | / =========men[0]== / men:|name | *---|-----> "Вася" | |---------------| | |town |M|s|c|\0| | |---------------| | |salary| 100 | | |---------------| | |addr | 12 | 7 | \ ---------------- \ =========men[1]== \-->|name | *---|-----> "Гриша"............

5.9. Составьте программу "справочник по таблице Менделеева", которая по названию химического эле мента выдавала бы его характеристики. Таблицу инициализируйте массивом структур.

5.10. При записи данных в файл (да и вообще) используйте структуры вместо массивов, если элементы массива имеют разное смысловое назначение. Не воспринимайте структуру просто как средство объеди нения данных разных типов, она может быть и средством объединения данных одного типа, если это доба вляет осмысленности нашей программе. Чем плох фрагмент?

int data[2];

data[0] = my_key;

data[1] = my_value;

write(fd, (char *) data, 2 * sizeof(int));

Во-первых, тогда уж лучше указать размер всего массива сразу (хотя бы на тот случай, если мы изменим его размер на 3 и забудем поправить множитель с 2 на 3).

write(fd, (char *) data, sizeof data);

Кстати, почему мы пишем data, а не &data? (ответ: потому что имя массива и есть его адрес). Во-вторых, элементы массива имеют разный смысл, так не использовать ли тут структуру?

struct _data { int key;

int value;

} data;

data.key = my_key;

data.value = my_value;

write(fd, &data, sizeof data);

5.11. Что напечатает следующая программа? Нарисуйте расположение указателей по окончании данной программы.

#include struct lnk{ char c;

struct lnk *prev, *next;

} chain[20], *head = chain;

add(c) char c;

{ head->c = c;

head->next = head+1;

head->next->prev = head;

head++;

} main(){ char *s = "012345";

А. Богатырёв, 1992-96 - 154 - Си в UNIX™ while( *s ) add( *s++ );

head->c = '-';

head->next = (struct lnk *)NULL;

chain->prev = chain->next;

while( head->prev ){ putchar( head->prev->c );

head = head->prev;

if( head->next ) head->next->prev = head->next->next;

} } 5.12. Напишите программу, составлящую двунаправленный список букв, вводимых с клавиатуры. Конец ввода - буква '\n'. После третьей буквы вставьте букву '+'. Удалите пятую букву. Распечатайте список в обратном порядке. Оформите операции вставки/удаления как функции. Элемент списка должен иметь вид:

struct elem{ char letter;

/* буква */ char *word;

/* слово */ struct elem *prev;

/* ссылка назад */ struct elem *next;

/* ссылка вперед */ };

struct elem *head, /* первый элемент списка */ *tail, /* последний элемент */ *ptr, /* рабочая переменная */ *prev;

/* предыдущий элемент при просмотре */ int c, cmp;

...

while((c = getchar()) != '\n' ) Insert(c, tail);

for(ptr=head;

ptr != NULL;

ptr=ptr->next) printf("буква %c\n", ptr->letter);

Память лучше отводить не из массива, а функцией calloc(), которая аналогична функции malloc(), но допол нительно расписывает выделенную память байтом '\0' (0, NULL). Вот функции вставки и удаления:

extern char *calloc();

/* создать новое звено списка для буквы c */ struct elem *NewElem(c) char c;

{ struct elem *p = (struct elem *) calloc(1, sizeof(struct elem));

/* calloc автоматически обнуляет все поля, * в том числе prev и next */ p->letter = c;

return p;

} /* вставка после ptr (обычно - после tail) */ Insert(c, ptr) char c;

struct elem *ptr;

{ struct elem *newelem = NewElem(c), *right;

if(head == NULL){ /* список был пуст */ head=tail=newelem;

return;

} right = ptr->next;

ptr->next = newelem;

newelem->prev = ptr;

newelem->next = right;

if( right ) right->prev = newelem;

else tail = newelem;

} /* удалить ptr из списка */ Delete( ptr ) struct elem *ptr;

{ struct elem *left=ptr->prev, *right=ptr->next;

if( right ) right->prev = left;

if( left ) left->next = right;

if( tail == ptr ) tail = left;

if( head == ptr ) head = right;

free((char *) ptr);

} А. Богатырёв, 1992-96 - 155 - Си в UNIX™ Напишите аналогичную программу для списка слов.

struct elem *NewElem(char *s) { struct elem *p = (struct elem *) calloc(1, sizeof(struct elem));

p->word = strdup(s);

return p;

} void DeleteElem(struct elem *ptr){ free(ptr->word);

free(ptr);

} Усложнение: вставляйте слова в список в алфавитном порядке. Используйте для этого функцию strcmp(), просматривайте список так:

struct elem *newelem;

if (head == NULL){ /* список пуст */ head = tail = NewElem(новое_слово);

return;

} /* поиск места в списке */ for(cmp= -1, ptr=head, prev=NULL;

ptr;

prev=ptr, ptr=ptr->next ) if((cmp = strcmp(новое_слово, ptr->word)) <= 0 ) break;

Если цикл окончился с cmp==0, то такое слово уже есть в списке. Если cmp < 0, то такого слова не было и ptr указывает элемент, перед которым надо вставить слово новое_слово, а prev - после которого (prev==NULL означает, что надо вставить в начало списка);

т.е. слово вставляется между prev и ptr. Если cmp > 0, то слово надо добавить в конец списка (при этом ptr==NULL).

head ==> "a" ==> "b" ==> "d" ==> NULL | | prev "c" ptr if(cmp == 0) return;

/* слово уже есть */ newelem = NewElem( новое_слово );

if(prev == NULL){ /* в начало */ newelem->next = head;

newelem->prev = NULL;

head->prev = newelem;

head = newelem;

} else if(ptr == NULL){ /* в конец */ newelem->next = NULL;

newelem->prev = tail;

tail->next = newelem;

tail = newelem;

} else { /* между prev и ptr */ newelem->next = ptr;

newelem->prev = prev;

prev->next = newelem;

ptr ->prev = newelem;

} 5.13. Напишите функции для работы с комплексными числами struct complex { double re, im;

};

Например, сложение выглядит так:

struct complex add( c1, c2 ) struct complex c1, c2;

{ struct complex sum;

А. Богатырёв, 1992-96 - 156 - Си в UNIX™ sum.re = c1.re + c2.re;

sum.im = c1.im + c2.im;

return sum;

} struct complex a = { 12.0, 14.0 }, b = { 13.0, 2.0 };

main(){ struct complex c;

c = add( a, b );

printf( "(%g,%g)\n", c.re, c.im );

} 5.14. Массивы в Си нельзя присваивать целиком, зато структуры - можно. Иногда используют такой трюк:

структуру из единственного поля-массива typedef struct { int ai[5];

} intarray5;

intarray5 a, b = { 1, 2, 3, 4, 5 };

и теперь законно a = b;

Зато доступ к ячейкам массива выглядит теперь менее изящно:

a.ai[2] = 14;

for(i=0;

i < 5;

i++) printf( "%d\n", a.ai[i] );

Также невозможно передать копию массива в качестве фактического параметра функции. Даже если мы напишем:

typedef int ARR16[16];

ARR16 d;

void f(ARR16 a){ printf( "%d %d\n", a[3], a[15]);

a[3] = 2345;

} void main(void){ d[3] = 9;

d[15] = 98;

f(d);

printf("Now it is %d\n", d[3]);

} то последний printf напечатает "Now it is 2345", поскольку в f передается адрес массива, но не его копия;

поэтому оператор a[3]=2345 изменяет исходный массив. Обойти это можно, использовав тот же трюк, поскольку при передаче структуры в качестве параметра передается уже не ее адрес, а копия всей струк туры (как это и принято в Си во всех случаях, кроме массивов).

5.15. Напоследок упомянем про битовые поля - элементы структуры, занимающие только часть машинного слова - только несколько битов в нем. Размер поля в битах задается конструкцией :число_битов. Битовые поля используются для более компактного хранения информации в структурах (для экономии места).

struct XYZ { /* битовые поля должны быть unsigned */ unsigned x:2;

/* 0.. 2**2 - 1 */ unsigned y:5;

/* 0.. 2**5 - 1 */ unsigned z:1;

/* YES=1 NO=0 */ } xyz;

main(){ printf("%u\n", sizeof(xyz));

/* == sizeof(int) */ xyz.z = 1;

xyz.y = 21;

xyz.x = 3;

printf("%u %u %u\n", xyz.x, ++xyz.y, xyz.z);

/* Значение битового поля берется по модулю * максимально допустимого числа 2**число_битов - */ xyz.y = 32 /* максимум */ + 7;

xyz.x = 16+2;

xyz.z = 11;

printf("%u %u %u\n", xyz.x, xyz.y, xyz.z);

/* 2 7 1 */ А. Богатырёв, 1992-96 - 157 - Си в UNIX™ } Поле ширины 1 часто используется в качестве битового флага: вместо #define FLAG1 #define FLAG2 #define FLAG3 int x;

/* слово для нескольких флагов */ x |= FLAG1;

x &= ~FLAG2;

if(x & FLAG3)...;

используется struct flags { unsigned flag1:1, flag2:1, flag3:1;

} x;

x.flag1 = 1;

x.flag2 = 0;

if( x.flag3 )...;

Следует однако учесть, что машинный код для работы с битовыми полями более сложен и занимает больше команд (т.е. медленнее и длиннее).

К битовым полям нельзя применить операцию взятия адреса "&", у них нет адресов и смещений!

5.16. Пример на использование структур с полем переменного размера. Часть переменной длины может быть лишь одна и обязана быть последним полем структуры. Внимание: это программистский трюк, использовать осторожно!

#include #define SZ extern char *malloc();

#define VARTYPE char struct obj { struct header { /* постоянная часть */ int cls;

int size;

/* размер переменной части */ } hdr;

VARTYPE body [1];

/* часть переменного размера:

в описании ровно ОДИН элемент массива */ } *items [SZ];

/* указатели на структуры */ #define OFFSET(field, ptr) ((char *) &ptr->field - (char *)ptr) int body_offset;

А. Богатырёв, 1992-96 - 158 - Си в UNIX™ /* создание новой структуры */ struct obj *newObj( int cl, char *s ) { char *ptr;

struct obj *op;

int n = strlen(s);

/* длина переменной части (штук VARTYPE) */ int newsize = sizeof(struct header) + n * sizeof(VARTYPE);

printf("[n=%d newsize=%d]\n", n, newsize);

/* newsize = (sizeof(struct obj) - sizeof(op->body)) + n * sizeof(op->body);

При использовании этого размера не учитывается, что struct(obj) выровнена на границу sizeof(int).

Но в частности следует учитывать и то, на границу чего выровнено начало поля op->body. То есть самым правильным будет newsize = body_offset + n * sizeof(op->body);

*/ /* отвести массив байт без внутренней структуры */ ptr = (char *) malloc(newsize);

/* наложить поверх него структуру */ op = (struct obj *) ptr;

op->hdr.cls = cl;

op->hdr.size = n;

strncpy(op->body, s, n);

return op;

} void printobj( struct obj *p ) { register i;

printf( "OBJECT(cls=%d,size=%d)\n", p->hdr.cls, p->hdr.size);

for(i=0;

i < p->hdr.size;

i++ ) putchar( p->body[i] );

putchar( '\n' );

} char *strs[] = { "a tree", "a maple", "an oak", "the birch", "the fir" };

int main(int ac, char *av[]){ int i;

printf("sizeof(struct header)=%d sizeof(struct obj)=%d\n", sizeof(struct header), sizeof(struct obj));

{ struct obj *sample;

printf("offset(cls)=%d\n", OFFSET(hdr.cls, sample));

printf("offset(size)=%d\n", OFFSET(hdr.size, sample));

printf("offset(body)=%d\n", body_offset = OFFSET(body, sample));

} А. Богатырёв, 1992-96 - 159 - Си в UNIX™ for( i=0;

i < SZ;

i++ ) items[i] = newObj( i, strs[i] );

for( i=0;

i < SZ;

i++ ){ printobj( items[i] );

free( items[i] );

items[i] = NULL;

} return 0;

} 5.17. Напишите программу, реализующую список со "старением". Элемент списка, к которому обраща лись последним, находится в голове списка. Самый старый элемент вытесняется к хвосту списка и в конеч ном счете из списка удаляется. Такой алгоритм использует ядро UNIX для кэширования блоков файла в оперативной памяти: блоки, к которым часто бывают обращения оседают в памяти (а не на диске).

/* Список строк, упорядоченных по времени их добавления в список, * т.е. самая "свежая" строка - в начале, самая "древняя" - в конце.

* Строки при поступлении могут и повторяться! По подобному принципу * можно организовать буферизацию блоков при обмене с диском.

*/ #include extern char *malloc(), *gets();

#define MAX 3 /* максимальная длина списка */ int nelems = 0;

/* текущая длина списка */ struct elem { /* СТРУКТУРА ЭЛЕМЕНТА СПИСКА */ char *key;

/* Для блоков - это целое - номер блока */ struct elem *next;

/* следующий элемент списка */ /*... и может что-то еще... */ } *head;

/* голова списка */ void printList(), addList(char *), forget();

void main(){ /* Введите a b c d b a c */ char buf[128];

while(gets(buf)) addList(buf), printList();

} /* Распечатка списка */ void printList(){ register struct elem *ptr;

printf( "В списке %d элементов\n", nelems );

for(ptr = head;

ptr != NULL;

ptr = ptr->next ) printf( "\t\"%s\"\n", ptr->key );

} /* Добавление в начало списка */ void addList(char *s) { register struct elem *p, *new;

/* Анализ - нет ли уже в списке */ for(p = head;

p != NULL;

p = p->next ) if( !strcmp(s, p->key)){ /* Есть. Перенести в начало списка */ if( head == p ) return;

/* Уже в начале */ /* Удаляем из середины списка */ new = p;

/* Удаляемый элемент */ for(p = head;

p->next != new;

p = p->next );

/* p указывает на предшественника new */ p->next = new->next;

goto Insert;

} /* Нет в списке */ if( nelems >= MAX ) forget();

/* Забыть старейший */ if((new = (struct elem *) malloc(sizeof(struct elem)))==NULL) goto bad;

if((new->key = malloc(strlen(s) + 1)) == NULL) goto bad;

strcpy(new->key, s);

nelems++;

Insert: new->next = head;

head = new;

return;

bad: printf( "Нет памяти\n" );

exit(13);

} А. Богатырёв, 1992-96 - 160 - Си в UNIX™ /* Забыть хвост списка */ void forget(){ struct elem *prev = head, *tail;

if( head == NULL ) return;

/* Список пуст */ /* Единственный элемент ? */ if((tail = head->next) == NULL){ tail=head;

head=NULL;

goto Del;

} for( ;

tail->next != NULL;

prev = tail, tail = tail->next );

prev->next = NULL;

Del: free(tail->key);

free(tail);

nelems--;

} А. Богатырёв, 1992-96 - 161 - Си в UNIX™ 6. Системные вызовы и взаимодействие с UNIX.

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

UNIX - многозадачная система (мультипрограммная). Это означает, что одновременно может быть запущено много процессов. Процессор выполняет их в режиме разделения времени - выделяя по очереди квант времени одному процессу, затем другому, третьему... В результате создается впечатление парал лельного выполнения всех процессов (на многопроцессорных машинах параллельность истинная). Процес сам, ожидающим некоторого события, время процессора не выделяется. Более того, "спящий" процесс может быть временно откачан (т.е. скопирован из памяти машины) на диск, чтобы освободить память для других процессов. Когда "спящий" процесс дождется события, он будет "разбужен" системой, переведен в ранг "готовых к выполнению" и, если был откачан - будет возвращен с диска в память (но, может быть, на другое место в памяти!). Эта процедура носит название "своппинг" (swapping).

Можно запустить несколько процессов, выполняющих программу из одного и того же файла;

при этом все они будут (если только специально не было предусмотрено иначе) независимыми друг от друга.

Так, у каждого пользователя, работающего в системе, имеется свой собственный процесс-интерпретатор команд (своя копия), выполняющий программу из файла /bin/csh (или /bin/sh).

Процесс представляет собой изолированный "мир", общающийся с другими "мирами" во Вселенной при помощи:

a) Аргументов функции main:

void main(int argc, char *argv[], char *envp[]);

Если мы наберем команду $ a.out a1 a2 a то функция main программы из файла a.out вызовется с = 4 /* количество аргументов */ argc = "a.out" argv[1] = "a1" argv[0] = "a2" argv[3] = "a3" argv[2] = NULL argv[4] По соглашению argv[0] содержит имя выполняемого файла из которого загружена эта программа†.

b) Так называемого "окружения" (или "среды") char *envp[], продублированного также в предопределен ной переменной extern char **environ;

Окружение состоит из строк вида "ИМЯПЕРЕМЕННОЙ=значение" Массив этих строк завершается NULL (как и argv). Для получения значения переменной с именем ИМЯ существует стандартная функция char *getenv( char *ИМЯ );

Она выдает либо значение, либо NULL если переменной с таким именем нет.

c) Открытых файлов. По умолчанию (неявно) всегда открыты 3 канала:

ВВОД ВЫВОД FILE * stdin stdout stderr соответствует fd 0 1 связан с клавиатурой дисплеем Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, программа может сама явно откры вать файлы (при помощи open, creat, pipe, fopen). Всего программа может одновременно открыть до 20 файлов (считая стандартные каналы), а в некоторых системах и больше (например, 64). В MS DOS есть еще 2 предопределенных канала вывода: stdaux - в последовательный коммуникационный порт, stdprn - на принтер.

d) Процесс имеет уникальный номер, который он может узнать вызовом int pid = getpid();

† Именно это имя показывает команда ps -ef #include main(ac, av) char **av;

{ execl("/bin/sleep", "Take it easy", "1000", NULL);

} А. Богатырёв, 1992-96 - 162 - Си в UNIX™ а также узнать номер "родителя" вызовом int ppid = getppid();

Процессы могут по этому номеру посылать друг другу сигналы:

kill(pid /* кому */, sig /* номер сигнала */);

и реагировать на них signal (sig /*по сигналу*/, f /*вызывать f(sig)*/);

e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая память, сете вые коммуникации.

f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий каталог, кото рый достается в наследство от процесса-"родителя", и может быть затем изменен системным вызо вом chdir(char *имя_нового_каталога);

У каждого процесса есть свой собственный текущий рабочий каталог (в отличие от MS DOS, где теку щий каталог одинаков для всех задач). К "прочим" характеристикам отнесем также: управляющий терминал;

группу процессов (pgrp);

идентификатор (номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции и маски, заданные на различные сигналы;

и.т.п.

g) Издания других запросов (системных вызовов) к операционной системе ("богу") для выполнения раз личных "внешних" операций.

h) Все остальные действия происходят внутри процесса и никак не влияют на другие процессы и устройства ("миры"). В частности, один процесс НИКАК не может получить доступ к памяти другого процесса, если тот не позволил ему это явно (механизм shared memory);

адресные пространства про цессов независимы и изолированы (равно и пространство ядра изолировано от памяти процессов).

Операционная система выступает в качестве коммуникационной среды, связывающей "миры"-процессы, "миры"-внешние устройства (включая терминал пользователя);

а также в качестве распо рядителя ресурсов "Вселенной", в частности - времени (по очереди выделяемого активным процессам) и пространства (в памяти компьютера и на дисках).

Мы уже неоднократно упоминали "системные вызовы". Что же это такое? С точки зрения Си программиста - это обычные функции. В них передают аргументы, они возвращают значения. Внешне они ничем не отличаются от написанных нами или библиотечных функций и вызываются из программ одинако вым с ними способом.

С точки же зрения реализации - есть глубокое различие. Тело функции-сисвызова расположено не в нашей программе, а в резидентной (т.е. постоянно находящейся в памяти компьютера) управляющей про грамме, называемой ядром операционной системы†. Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия", т.е. вызов функции в ядре системы. Ядро работает в привеле гированном режиме, в котором имеет доступ к некоторым системным таблицам‡, регистрам и портам † Собственно, операционная система характеризуется набором предоставляемых ею системных вызо вов, поскольку все концепции, заложенные в системе, доступны нам только через них. Если мы имеем две реализации системы с разным внутренним устройством ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться, но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX ами на однопроцессорных и многопроцессорных машинах. Но для нас ядро - это "черный ящик", полностью определяемый его поведением, т.е. своим интерфейсом с программами, но не внутренним устройством.

Вторым параметром, характеризующим ОС, являются форматы данных, используемые системой: форматы данных для сисвызовов и формат информации в различных файлах, в том числе формат оформления вы полняемых файлов (формат данных в физической памяти машины в этот список не входит - он зависим от реализации и от процессора). Как правило, программа пишется так, чтобы использовать соглашения, при нятые в данной системе, для чего она просто включает ряд стандартных include-файлов с описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы.

Поведение всех программ в системе вытекает из поведения системных вызовов, которыми они поль зуются. Даже то, что UNIX является многозадачной системой, непосредственно вытекает из наличия си стемных вызовов fork, exec, wait и спецификации их функционирования!

То же можно сказать про язык Си - мобильность программы зависит в основном от набора использу емых в ней библиотечных функций (и, в меньшей степени, от диалекта самого языка, который должен удо влетворять стандарту на язык Си). Если две разные системы предоставляют все эти функции (которые мо гут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково.

‡ Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого процесса), и.т.п.

А. Богатырёв, 1992-96 - 163 - Си в UNIX™ внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в пользовательской программе вызывается библиотечная функция-"корешок", тело которой написано на ассемблере и содержит команду генерации программного прерывания. Это - главное отличие от нормальных Си-функций - вызов по прерыванию. Вто рым этапом является реакция ядра на прерывание:

1. переход в привелегированный режим;

2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к адресному про странству ядра (context switching);

3. извлечение аргументов из памяти запросившего процесса;

4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам - это номер системного вызова);

5. проверка корректности остальных аргументов;

6. проверка прав процесса на допустимость выполнения такого запроса;

7. вызов тела требуемого системного вызова - это обычная Си-функция в ядре;

8. возврат ответа в память процесса;

9. выключение привелегированного режима;

10. возврат из прерывания.

Во время системного вызова (шаг 7) процесс может "заснуть", дожидаясь некоторого события (например, нажатия кнопки на клавиатуре). В это время ядро передаст управление другому процессу.

Когда наш процесс будет "разбужен" (событие произошло) - он продолжит выполнение шагов системного вызова.

Большинство системных вызовов возвращают в программу в качестве своего значения признак успеха: 0 - все сделано, (-1) - сисвызов завершился неудачей;

либо некоторое содержательное значение при успехе (вроде дескриптора файла в open(), и (-1) при неудаче. В случае неудачного завершения в предопределенную переменную errno заносится номер ошибки, описывающий причину неудачи (коды оши бок предопределены, описаны в include-файле и имеют вид Eчтото). Заметим, что при УДАЧЕ эта переменная просто не изменяется и может содержать любой мусор, поэтому проверять ее имеет смысл лишь в случае, если ошибка действительно произошла:

#include /* коды ошибок */ extern int errno;

extern char *sys_errlist[];

int value;

if((value = sys_call(...)) < 0 ){ printf("Error:%s(%d)\n", sys_errlist[errno], errno );

exit(errno);

/* принудительное завершение программы */ } Предопределенный массив sys_errlist, хранящийся в стандартной библиотеке, содержит строки расшифровку смысла ошибок (по-английски). Посмотрите описание функции perror().

6.1. Файлы и каталоги.

6.1.1. Используя системный вызов stat, напишите программу, определяющую тип файла: обычный файл, каталог, устройство, FIFO-файл. Ответ:

#include #include typeOf( name ) char *name;

{ int type;

struct stat st;

if( stat( name, &st ) < 0 ){ printf( "%s не существует\n", name );

return 0;

} printf("Файл имеет %d имен\n", st.st_nlink);

switch(type = (st.st_mode & S_IFMT)){ case S_IFREG:

А. Богатырёв, 1992-96 - 164 - Си в UNIX™ printf( "Обычный файл размером %ld байт\n", st.st_size );

break;

case S_IFDIR:

printf( "Каталог\n" );

break;

case S_IFCHR: /* байтоориентированное */ case S_IFBLK: /* блочноориентированное */ printf( "Устройство\n" );

break;

case S_IFIFO:

printf( "FIFO-файл\n" );

break;

default:

printf( "Другой тип\n" );

break;

} return type;

} 6.1.2. Напишите программу, печатающую: свои аргументы, переменные окружения, информацию о всех открытых ею файлах и используемых трубах. Для этой цели используйте системный вызов struct stat st;

int used, fd;

for(fd=0;

fd < NOFILE;

fd++ ){ used = fstat(fd, &st) < 0 ? 0 : 1;

...

} Программа может использовать дескрипторы файлов с номерами 0..NOFILE-1 (обычно 0..19). Если fstat для какого-то fd вернул код ошибки (<0), это означает, что данный дескриптор не связан с открытым фай лом (т.е. не используется). NOFILE определено в include-файле , содержащем разнообразные параметры данной системы.

6.1.3. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего каталога (файла с именем ".") без сортировки имен по алфавиту. Предусмотрите чтение каталога, чье имя задается как аргумент программы. Имена "." и ".." не выдавать.

Формат каталога описан в header-файле и в "канонической" версии выглядит так: каталог - это файл, состоящий из структур direct, каждая описывает одно имя файла, входящего в каталог:

struct direct { unsigned short d_ino;

/* 2 байта: номер I-узла */ char d_name[DIRSIZ];

/* имя файла */ };

В семействе BSD формат каталога несколько иной - там записи имеют разную длину, зависящую от длины имени файла, которое может иметь длину от 1 до 256 символов.

Имя файла может состоять из любых символов, кроме '\0', служащего признаком конца имени и '/', служащего разделителем. В имени допустимы пробелы, управляющие символы (но не рекомендуются!), любое число точек (в отличие от MS DOS, где допустима единственная точка, отделяющая собственно имя от суффикса (расширения)), разрешены даже непечатные (т.е. управляющие) символы! Если имя файла имеет длину 14 (DIRSIZ) символов, то оно не оканчивается байтом '\0'. В этом случае для печати имени файла возможны три подхода:

1. Выводить символы при помощи putchar()-а в цикле. Цикл прерывать по индексу равному DIRSIZ, либо по достижению байта '\0'.

2. Скопировать поле d_name в другое место:

char buf[ DIRSIZ + 1 ];

strncpy(buf, d.d_name, DIRSIZ);

buf[ DIRSIZ ] = '\0';

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

3. Использовать такую особенность функции printf():

#include #include struct direct d;

...

printf( "%*.*s\n", DIRSIZ, DIRSIZ, d.d_name );

А. Богатырёв, 1992-96 - 165 - Си в UNIX™ Если файл был стерт, то в поле d_ino записи каталога будет содержаться 0 (именно поэтому I-узлы нумеруются начиная с 1, а не с 0). При удалении файла содержимое его (блоки) уничтожается, I-узел осво бождается, но имя в каталоге не затирается физически, а просто помечается как стертое: d_ino=0;

Каталог при этом никак не уплотняется и не укорачивается! Поэтому имена с d_ino==0 выдавать не следует - это имена уже уничтоженных файлов.

При создании нового имени (creat, link, mknod) система просматривает каталог и переиспользует первый от начала свободный слот (ячейку каталога) где d_ino==0, записывая новое имя в него (только в этот момент старое имя-призрак окончательно исчезнет физически). Если пустых мест нет - каталог удли няется.

Любой каталог всегда содержит два стандартных имени: "." - ссылка на этот же каталог (на его соб ственный I-node), ".." - на вышележащий каталог. У корневого каталога "/" оба этих имени ссылаются на него же самого (т.е. содержат d_ino==2).

Имя каталога не содержится в нем самом. Оно содержится в "родительском" каталоге...

Каталог в UNIX - это обычный дисковый файл. Вы можете читать его из своих программ. Однако никто (включая суперпользователя‡) не может записывать что-либо в каталог при помощи write. Измене ния содержимого каталогов выполняет только ядро, отвечая на запросы в виде системных вызовов creat, unlink, link, mkdir, rmdir, rename, mknod. Коды доступа для каталога интерпретируются следующим обра зом:

w запись S_IWRITE. Означает право создавать и уничтожать в каталоге имена файлов при помощи этих вызо вов. То есть: право создавать, удалять и переименовывать файлы в каталоге. Отметим, что для пере именования или удаления файла вам не требуется иметь доступ по записи к самому файлу - доста точно иметь доступ по записи к каталогу, содержащему его имя!

r чтение S_IREAD. Право читать каталог как обычный файл (право выполнять opendir, см. ниже): благодаря этому мы можем получить список имен файлов, содержащихся в каталоге. Однако, если мы ЗАРАНЕЕ знаем имена файлов в каталоге, мы МОЖЕМ работать с ними - если имеем право доступа "выполне ние" для этого каталога!

x выполнение S_IEXEC. Разрешает поиск в каталоге. Для открытия файла, создания/удаления файла, перехода в другой каталог (chdir), система выполняет следующие действия (осуществляемые функцией namei() в ядре): чтение каталога и поиск в нем указанного имени файла или каталога;

найденному имени соот ветствует номер I-узла d_ino;

по номеру узла система считывает с диска сам I-узел нужного файла и по нему добирается до содержимого файла. Код "выполнение" - это как раз разрешение такого про смотра каталога системой. Если каталог имеет доступ на чтение - мы можем получить список фай лов (т.е. применить команду ls);

но если он при этом не имеет кода доступа "выполнение" - мы не сможем получить доступа ни к одному из файлов каталога (ни открыть, ни удалить, ни создать, ни сделать stat, ни chdir). Т.е. "чтение" разрешает применение вызова read, а "выполнение" - функции ядра namei. Фактически "выполнение" означает "доступ к файлам в данном каталоге";

еще более точно - к I-nodам файлов этого каталога.

t sticky bit S_ISVTX - для каталога он означает, что удалить или переименовать некий файл в данном каталоге могут только: владелец каталога, владелец данного файла, суперпользователь. И никто другой. Это исключает удаление файлов чужими.

Совет: для каталога полезно иметь такие коды доступа:

chmod o-w,+t каталог В системах BSD используется, как уже было упомянуто, формат каталога с переменной длиной записей.

Чтобы иметь удобный доступ к именам в каталоге, возникли специальные функции чтения каталога: opendir, closedir, readdir. Покажем, как простейшая команда ls реализуется через эти функции.

‡ Суперпользователь (superuser) имеет uid==0. Это "привелегированный" пользователь, который имеет право делать ВСЕ. Ему доступны любые сисвызовы и файлы, несмотря на коды доступа и.т.п.

А. Богатырёв, 1992-96 - 166 - Си в UNIX™ #include #include #include int listdir(char *dirname){ register struct dirent *dirbuf;

DIR *fddir;

ino_t dot_ino = 0, dotdot_ino = 0;

if((fddir = opendir (dirname)) == NULL){ fprintf(stderr, "Can't read %s\n", dirname);

return 1;

} /* Без сортировки по алфавиту */ while ((dirbuf = readdir (fddir)) != NULL ) { if (dirbuf->d_ino == 0) continue;

if (strcmp (dirbuf->d_name, "." ) == 0){ dot_ino = dirbuf->d_ino;

continue;

} else if(strcmp (dirbuf->d_name, "..") == 0){ dotdot_ino = dirbuf->d_ino;

continue;

} else printf("%s\n", dirbuf->d_name);

} closedir (fddir);

if(dot_ino == 0) printf("Поврежденный каталог: нет имени \".\"\n");

if(dotdot_ino == 0) printf("Поврежденный каталог: нет имени \"..\"\n");

if(dot_ino && dot_ino == dotdot_ino) printf("Это корневой каталог диска\n");

return 0;

} int main(int ac, char *av[]){ int i;

if(ac > 1) for(i=1;

i < ac;

i++) listdir(av[i]);

else listdir(".");

return 0;

} Обратите внимание, что тут не требуется добавление '\0' в конец поля d_name, поскольку его предоста вляет нам сама функция readdir().

6.1.4. Напишите программу удаления файлов и каталогов, заданных в argv. Делайте stat, чтобы опреде лить тип файла (файл/каталог). Программа должна отказываться удалять файлы устройств.

Для удаления пустого каталога (не содержащего иных имен, кроме "." и "..") следует использовать сисвызов rmdir(имя_каталога);

(если каталог не пуст - errno получит значение EEXIST);

а для удаления обычных файлов (не каталогов) unlink(имя_файла);

Программа должна запрашивать подтверждение на удаление каждого файла, выдавая его имя, тип, размер в килобайтах и вопрос "удалить ?".

6.1.5. Напишите функцию рекурсивного обхода дерева подкаталогов и печати имен всех файлов в нем.

Ключ U42 означает файловую систему с длинными именами файлов (BSD 4.2).

А. Богатырёв, 1992-96 - 167 - Си в UNIX™ /*#!/bin/cc -DFIND -DU42 -DMATCHONLY treemk.c match.c -o tree -lx * Обход поддерева каталогов (по мотивам Керниган & Ритчи).

* Ключи компиляции:

* BSD-4.2 BSD-4.3 -DU * XENIX с канонической файл.сист. ничего * XENIX с библиотекой -lx -DU * программа поиска файлов -DFIND * программа рекурсивного удаления -DRM_REC * программа подсчета используемого места на диске БЕЗ_КЛЮЧА */ #include #include #include #include /* для MAXPATHLEN */ #if defined(M_XENIX) && defined(U42) # include /* XENIX + U42 эмуляция */ #else # include # define stat(f,s) lstat(f,s) /* не проходить по символьным ссылкам */ # define d_namlen d_reclen #endif /* проверка: каталог ли это */ #define isdir(st) ((st.st_mode & S_IFMT) == S_IFDIR) struct stat st;

/* для сисвызова stat() */ char buf[MAXPATHLEN+1];

/* буфер для имени файла */ #define FAILURE (-1) /* код неудачи */ #define SUCCESS 1 /* код успеха */ #define WARNING 0 /* нефатальная ошибка */ /* Сообщения об ошибках во время обхода дерева: */ #ifndef ERR_CANT_READ # define ERR_CANT_READ(name) \ fprintf( stderr, "\tНе могу читать \"%s\"\n", name), WARNING # define ERR_NAME_TOO_LONG() \ fprintf( stderr, "\tСлишком длинное полное имя\n" ), WARNING #endif /* Прототипы для предварительного объявления функций. */ extern char *strrchr(char *, char);

int directory (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st));

/* Функции-обработчики enter, leave, touch должны * возвращать (-1) для прерывания просмотра дерева, * либо значение >= 0 для продолжения. */ /* Обойти дерево с корнем в rootdir */ int walktree ( char *rootdir, /* корень дерева */ int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st) ){ /* проверка корректности корня */ if( stat(rootdir, &st) < 0 || !isdir(st)){ fprintf( stderr, "\tПлохой корень дерева \"%s\"\n", rootdir );

return FAILURE;

/* неудача */ } strcpy (buf, rootdir);

return act (buf, 0, enter, leave, touch);

} А. Богатырёв, 1992-96 - 168 - Си в UNIX™ /* Оценка файла с именем name.

*/ int act (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st)) { if (stat (name, &st) < 0) return WARNING;

/* ошибка, но не фатальная */ if(isdir(st)){ /* позвать обработчик каталогов */ if(enter) if( enter(name, level, &st) == FAILURE ) return FAILURE;

return directory (name, level+1, enter, leave, touch);

} else { /* позвать обработчик файлов */ if(touch) return touch (name, level, &st);

else return SUCCESS;

} } А. Богатырёв, 1992-96 - 169 - Си в UNIX™ /* Обработать каталог: прочитать его и найти подкаталоги */ int directory (char *name, int level, int (*enter)(char *full, int level, struct stat *st), int (*leave)(char *full, int level), int (*touch)(char *full, int level, struct stat *st)) { #ifndef U struct direct dirbuf;

int fd;

#else register struct dirent *dirbuf;

DIR *fd;

extern DIR *opendir();

#endif char *nbp, *tail, *nep;

int i, retcode = SUCCESS;

#ifndef U if ((fd = open (name, 0)) < 0) { #else if ((fd = opendir (name)) == NULL) { #endif return ERR_CANT_READ(name);

} tail = nbp = name + strlen (name);

/* указатель на закрывающий \0 */ if( strcmp( name, "/" )) /* если не "/" */ *nbp++ = '/';

*nbp = '\0';

#ifndef U if (nbp + DIRSIZ + 2 >= name + MAXPATHLEN) { *tail = '\0';

return ERR_NAME_TOO_LONG();

} #endif #ifndef U while (read(fd, (char *) &dirbuf, sizeof(dirbuf)) == sizeof(dirbuf)){ if (dirbuf.d_ino == 0) /* стертый файл */ continue;

if (strcmp (dirbuf.d_name, "." ) == 0 || strcmp (dirbuf.d_name, "..") == 0) /* не интересуют */ continue;

for (i = 0, nep = nbp;

i < DIRSIZ;

i++) *nep++ = dirbuf.d_name[i];

# else /*U42*/ while ((dirbuf = readdir (fd)) != NULL ) { if (dirbuf->d_ino == 0) continue;

if (strcmp (dirbuf->d_name, "." ) == 0 || strcmp (dirbuf->d_name, "..") == 0) continue;

for (i = 0, nep = nbp;

i < dirbuf->d_namlen ;

i++) *nep++ = dirbuf->d_name[i];

#endif /*U42*/ *nep = '\0';

if( act(name, level, enter, leave, touch) == FAILURE) { retcode = FAILURE;

break;

} } #ifndef U close (fd);

#else closedir(fd);

#endif А. Богатырёв, 1992-96 - 170 - Си в UNIX™ *tail = '\0';

/* восстановить старое name */ if(retcode != FAILURE && leave) if( leave(name, level) == FAILURE) retcode = FAILURE;

return retcode;

} /* -------------------------------------------------------------- */ /* Disk Usage -- Оценка места, занимаемого файлами поддерева */ /* -------------------------------------------------------------- */ /* Пересчет байтов в килобайты */ #define KB(s) (((s)/1024L) + ((s)%1024L ? 1L:0L)) /* или #define KB(s) (((s) + 1024L - 1) / 1024L) */ long size;

/* общий размер */ long nfiles;

/* всего файлов */ long ndirs;

/* из них каталогов */ #define WARNING_LIMIT 150L /* подозрительно большой файл */ static int du_touch (char *name, int level, struct stat *st){ long sz;

size += (sz = KB(st->st_size));

/* размер файла в Кб. */ nfiles++;

#ifndef TREEONLY if( sz >= WARNING_LIMIT ) fprintf(stderr,"\tВнимание! \"%s\" очень большой: %ld Кб.\n", name, sz);

#endif /*TREEONLY*/ return SUCCESS;

} static int du_enter (char *name, int level, struct stat *st){ #ifndef TREEONLY fprintf( stderr, "Каталог \"%s\"\n", name );

#endif size += KB(st->st_size);

/* размер каталога в Кб. */ nfiles++;

++ndirs;

return SUCCESS;

} long du (char *name){ size = nfiles = ndirs = 0L;

walktree(name, du_enter, NULL, du_touch );

return size;

} /* -------------------------------------------------------------- */ /* Рекурсивное удаление файлов и каталогов */ /* -------------------------------------------------------------- */ int deleted;

/* сколько файлов и каталогов удалено */ static int recrm_dir (char *name, int level){ if( rmdir(name) >= 0){ deleted++;

return SUCCESS;

} fprintf(stderr, "Не могу rmdir '%s'\n", name);

return WARNING;

} static int recrm_file(char *name, int level, struct stat *st){ if( unlink(name) >= 0){ deleted++;

return SUCCESS;

} fprintf(stderr, "Не могу rm '%s'\n", name);

return WARNING;

} int recrmdir(char *name){ int ok_code;

deleted = 0;

ok_code = walktree(name, NULL, recrm_dir, recrm_file);

printf("Удалено %d файлов и каталогов в %s\n", deleted, name);

return ok_code;

} А. Богатырёв, 1992-96 - 171 - Си в UNIX™ /* -------------------------------------------------------------- */ /* Поиск файлов с подходящим именем (по шаблону имени) */ /* -------------------------------------------------------------- */ char *find_PATTERN;

static int find_check(char *fullname, int level, struct stat *st){ char *basename = strrchr(fullname, '/');

if(basename) basename++;

else basename = fullname;

if( match(basename, find_PATTERN)) printf("Level#%02d %s\n", level, fullname);

if( !strcmp( basename, "core")){ printf("Найден дамп %s, поиск прекращен.\n", fullname);

return FAILURE;

} return SUCCESS;

} void find (char *root, char *pattern){ find_PATTERN = pattern;

walktree(root, find_check, NULL, find_check);

} /* -------------------------------------------------------------- */ #ifndef TREEONLY void main(int argc, char *argv[]){ #ifdef FIND if(argc != 3){ fprintf(stderr, "Arg count\n");

exit(1);

} find(argv[1], argv[2]);

#else # ifdef RM_REC for(argv++;

*argv;

argv++) recrmdir(*argv);

# else du( argc == 1 ? "." : argv[1] );

printf( "%ld килобайт в %ld файлах.\n", size, nfiles );

printf( "%ld каталогов.\n", ndirs );

# endif #endif exit(0);

} #endif /*TREEONLY*/ 6.1.6. Используя предыдущий алгоритм, напишите программу рекурсивного копирования поддерева ката логов в другое место. Для создания новых каталогов используйте системный вызов mkdir(имя_каталога, коды_доступа);

6.1.7. Используя тот же алгоритм, напишите программу удаления каталога, которая удаляет все файлы в нем и, рекурсивно, все его подкаталоги. Таким образом, удаляется дерево каталогов. В UNIX подобную операцию выполняет команда rm -r имя_каталога_корня_дерева 6.1.8. Используя все тот же алгоритм обхода, напишите аналог команды find, который будет позволять:

• находить все файлы, чьи имена удовлетворяют заданному шаблону (используйте функцию match() из главы "Текстовая обработка");

• находить все выполняемые файлы: обычные файлы S_IFREG, у которых (st.st_mode & 0111) != Как уже ясно, следует пользоваться вызовом stat для проверки каждого файла.

6.2. Время в UNIX.

6.2.1. Напишите функцию, переводящую год, месяц, день, часы, минуты и секунды в число секунд, прошед шее до указанного момента с 00 часов 00 минут 00 секунд 1 Января 1970 года. Внимание: результат дол жен иметь тип long (точнее time_t).

А. Богатырёв, 1992-96 - 172 - Си в UNIX™ Эта функция облегчит вам сравнение двух моментов времени, заданных в общепринятом "человече ском" формате, поскольку сравнить два long числа гораздо проще, чем сравнивать по очереди годы, затем, если они равны - месяцы, если месяцы равны - даты, и.т.д.;

а также облегчит измерение интервала между двумя событиями - он вычисляется просто как разность двух чисел. В системе UNIX время обрабатывается и хранится именно в виде числа секунд;

в частности текущее астрономическое время можно узнать систем ным вызовом #include #include time_t t = time(NULL);

/* time(&t);

*/ Функция struct tm *tm = localtime( &t );

разлагает число секунд на отдельные составляющие, содержащиеся в int-полях структуры:

Pages:     | 1 | 2 || 4 | 5 |   ...   | 6 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.