4. Работа с файлами

Файлы представляют собой области памяти на внешнем носителе (как правило магнитном диске), предназначенные для:

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

Файлы отличаются от обычных массивов тем, что

Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и простых файлов. Об этом и о системе именования файлов прочитайте в документации по UNIX.

4.1.

Для работы с каким-либо файлом наша программа должна открыть этот файл - установить связь между именем файла и некоторой переменной в программе. При открытии файла в ядре операционной системы выделяется "связующая" структура file "открытый файл", содержащая:
f_offset: указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как
RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи;
f_flag: режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги;
f_inode: расположение файла на диске (в UNIX - в виде ссылки на I-узел файла*);
и кое-что еще.

У каждого процесса имеется таблица открытых им файлов - это массив ссылок на упомянутые "связующие" структуры**. При открытии файла в этой таблице ищется

      - длина файла                  long   di_size;
      - номер владельца файла        int    di_uid;
      - коды доступа и тип файла     ushort di_mode;
      - время создания и последней модификации
                    time_t di_ctime, di_mtime;
      - начало таблицы блоков файла  char   di_addr[...];
      - количество имен файла        short  di_nlink;
      и.т.п.

Содержимое некоторых полей этого паспорта можно узнать вызовом stat(). Все I-узлы собраны в единую область в начале файловой системы - так называемый I-файл. Все Iузлы пронумерованы, начиная с номера 1. Корневой каталог (файл с именем "/") как правило имеет I-узел номер 2.

** - У каждого процесса в UNIX также есть свой "паспорт". Часть этого паспорта находится в таблице процессов в ядре ОС, а часть - "приклеена" к самому процессу, однако не доступна из программы непосредственно. Эта вторая часть паспорта носит название "u-area" или структура user. В нее, в частности, входят таблица открытых процессом файлов, свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого "дескриптора файла".

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

Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных программах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь: несколько или один процессов могут открыть один и тот же файл одновременно несколько раз. При этом будет создано несколько "связующих" структур (по одной для каждого открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуация, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание вызова dup2.

     fd   u_ofile[]          struct file
      0   ##                 ------------ 1---##---------------->| f_flag    |
      2   ##                 | f_count=3 |
      3---##---------------->| f_inode---------*
     ...  ## *-------------->| f_offset  |     |
    процесс1 |               ------!------     |
             |                     !           V
      0   ## |  struct file        !   struct inode
      1   ## |  -------------      !   -------------
      2---##-*  | f_flag    |      !   | i_count=2 |
      3---##--->| f_count=1 |      !   | i_addr[]----*
     ...  ##    | f_inode----------!-->|    ...    | | адреса
    процесс2    | f_offset  |      !   ------------- | блоков
                -------!-----      *=========*       | файла
                       !                     !       V
            0          !   указатели R/W     !   i_size-1
            @@@@@@@@@@@!@@@@@@@@@@@@@@@@@@@@@!@@@@@@
                           файл на диске
    /* открыть файл */
    int fd = open(char имя_файла[], int как_открыть);
            ...  /* какие-то операции с файлом */
    close(fd);  /* закрыть */
Параметр как_открыть:
    #include <fcntl.h>
    O_RDONLY  - только для чтения.
    O_WRONLY  - только для записи.
    O_RDWR    - для чтения и записи.
    O_APPEND  - иногда используется вместе с
    открытием для записи, "добавление" в файл:
        O_WRONLY|O_APPEND, O_RDWR|O_APPEND
Если файл еще не существовал, то его нельзя открыть: open вернет значение (-1),
     struct file *u_ofile[NOFILE];
ссылка на I-узел текущего каталога
     struct inode *u_cdir;
а также ссылка на часть паспорта в таблице процессов
     struct proc *u_procp;
сигнализирующее об ошибке. В этом случае файл надо создать:
    int fd = creat(char имя_файла[], int коды_доступа);

Дескриптор fd будет открыт для записи в этот новый пустой файл. Если же файл уже существовал, creat опустошает его, т.е. уничтожает его прежнее содержимое и делает его длину равной 0L байт. Коды_доступа задают права пользователей на доступ к файлу. Это число задает битовую шкалу из 9и бит, соответствующих строке

    биты:   876 543 210
            rwx rwx rwx
    r - можно читать файл
    w - можно записывать в файл
    x - можно выполнять программу из этого файла

Первая группа - эта права владельца файла, вторая - членов его группы, третяя - всех прочих. Эти коды для владельца файла имеют еще и мнемонические имена (используемые в вызове stat):

    #include <sys/stat.h>  /* Там определено: */
    #define S_IREAD           0400
    #define S_IWRITE          0200
    #define S_IEXEC           0100

Подробности - в руководствах по системе UNIX. Отметим в частности, что open() может вернуть код ошибки fd < 0 не только в случае, когда файл не существует (errno==ENOENT), но и в случае, когда вам не разрешен соответствующий доступ к этому файлу (errno==EACCES; про переменную кода ошибки errno см. в главе "Взаимодействие с UNIX").

Вызов creat - это просто разновидность вызова open в форме

    fd = open( имя_файла,
               O_WRONLY|O_TRUNC|O_CREAT, коды_доступа);
O_TRUNC
означает, что если файл уже существует, то он должен быть опустошен при открытии. Коды доступа и владелец не изменяются.
O_CREAT
означает, что файл должен быть создан, если его не было (без этого флага файл не создастся, а open вернет fd < 0). Этот флаг требует задания третьего аргумента
коды_доступа***.
Если файл уже существует - этот флаг не имеет никакого эффекта, но зато вступает в действие O_TRUNC.

Существует также флаг

O_EXCL
который может использоваться совместно с O_CREAT. Он делает следующее: если файл уже существует, open вернет код ошибки (errno==EEXIST). Если файл не

*** - Заметим, что на самом деле коды доступа у нового файла будут равны

di_mode = (коды_доступа & ~u_cmask) | IFREG;
(для каталога вместо IFREG будет IFDIR), где маска u_cmask задается системным вызовом
umask(u_cmask);
(вызов выдает прежнее значение маски) и в дальнейшем наследуется всеми потомками данного процесса (она хранится в u-area процесса). Эта маска позволяет запретить доступ к определенным операциям для всех создаваемых нами файлов, несмотря на явно заданные коды доступа, например
umask(0077); /* ???------ */
делает значащими только первые 3 бита кодов доступа (для владельца файла). Остальные биты будут равны нулю.

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

существовал - срабатывает O_CREAT и файл создается.
Это позволяет предохранить уже существующие файлы от уничтожения.
Файл удаляется при помощи
    int unlink(char имя_файла[]);
У каждой программы по умолчанию открыты три первых дескриптора, обычно связанные
    0 - с клавиатурой (для чтения)
    1 - с дисплеем    (выдача результатов)
    2 - с дисплеем    (выдача сообщений об ошибках)
Если при вызове close(fd) дескриптор fd не соответствует открытому файлу (не был открыт) - ничего не происходит.

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

4.2.

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

Читайте и записывайте файлы большими кусками, кратными 512 байтам. Это уменьшит число обращений к диску. Схема:

    char buffer[512]; int n; int fd_inp, fd_outp;
            ...
    while((n = read (fd_inp,  buffer, sizeof buffer)) > 0)
               write(fd_outp, buffer, n);
Приведем несколько примеров использования write:
    char c = 'a';
    int  i = 13, j = 15;
    char s[20] = "foobar";
    char p[]   = "FOOBAR";
    struct { int x, y; } a = { 666, 999 };
    /* создаем файл с доступом    rw-r--r-- */
    int fd = creat("aFile", 0644);
    write(fd, &c, 1);
    write(fd, &i, sizeof i);  write(fd, &j, sizeof(int));
    write(fd, s,  strlen(s)); write(fd, &a, sizeof a);
    write(fd, p,  sizeof(p) - 1);
    close(fd);
Обратите внимание на такие моменты:

4.2.1. m = write(fd, addr, n);

    если( ФАЙЛ[fd] не открыт на запись) то вернуть (-1);
    если(n == 0) то вернуть 0;
    если( ФАЙЛ[fd] открыт на запись с флагом O_APPEND ) то
      RWptr = длина_файла; /* т.е. встать на конец файла */
    если( RWptr > длина_файла ) то
      заполнить нулями байты файла в интервале
      ФАЙЛ[fd][ длина_файла..RWptr-1 ] = '\0';
    скопировать байты из памяти процесса в файл
      ФАЙЛ[fd][ RWptr..RWptr+n-1 ] = addr[ 0..n-1 ];
      отводя на диске новые блоки, если надо
    RWptr += n;
    если( RWptr > длина_файла ) то
          длина_файла = RWptr;
    вернуть n;

4.2.2. m = read(fd, addr, n);

    если( ФАЙЛ[fd] не открыт на чтение) то вернуть (-1);
    если( RWptr >= длина_файла ) то вернуть 0;
    m = MIN( n, длина_файла - RWptr );
    скопировать байты из файла в память процесса
      addr[ 0..m-1 ] = ФАЙЛ[fd][ RWptr..RWptr+m-1 ];
    RWptr += m;
    вернуть m;

4.3.

Найдите ошибки в фрагменте программы:
    #define STDOUT 1  /* дескриптор стандартного вывода */
    int i;
    static char s[20] = "hi\n";
    char c = '\n';
    struct a{ int x,y; char ss[5]; } po;
    scanf( "%d%d%d%s%s", i, po.x, po.y, s, po.ss);
    write( STDOUT, s, strlen(s));
    write( STDOUT, c, 1 );       /* записать 1 байт */

Ответ: в функции scanf перед аргументом i должна стоять операция "адрес", то есть &i. Аналогично про &po.x и &po.y. Заметим, что s - это массив, т.е. s и так есть адрес, поэтому перед s операция & не нужна; аналогично про po.ss - здесь & не требуется.

В системном вызове write второй аргумент должен быть адресом данного, которое мы хотим записать в файл. Поэтому мы должны были написать &c (во втором вызове write).

Ошибка в scanf - указание значения переменной вместо ее адреса - является довольно распространенной и не может быть обнаружена компилятором (даже при использовании прототипа функции scanf(char *fmt, ...), так как scanf - функция с переменным числом аргументов заранее не определенных типов). Приходится полагаться исключительно на собственную внимательность!

4.4.

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

    #include <fcntl.h>
    #include <stdio.h>
    #include <sys/param.h> /* там определено NOFILE */
    #include <errno.h>
    char *typeOfOpen(fd){
      int flags;
      if((flags=fcntl (fd, F_GETFL, NULL)) < 0 )
        return NULL;  /* fd вероятно не открыт */
      flags &= O_RDONLY | O_WRONLY | O_RDWR;
      switch(flags){
      case O_RDONLY:  return "r";
      case O_WRONLY:  return "w";
      case O_RDWR:    return "r+w";
      default:        return NULL;
      }
    }
    char *type2OfOpen(fd){
      extern errno; /* см. главу "системные вызовы" */
      int r=1, w=1;
      errno = 0; read(fd, NULL, 0);
      if( errno == EBADF ) r = 0;
      errno = 0; write(fd, NULL, 0);
      if( errno == EBADF ) w = 0;
      return (w && r) ? "r+w" :
              w       ? "w"   :
              r       ? "r"   :
                        "closed";
    }
    main(){
      int i; char *s, *p;
      for(i=0; i < NOFILE; i++ ){
         s = typeOfOpen(i); p = type2OfOpen(i);
        printf("%d:%s %s\n", i, s? s: "closed", p);
      }
    }

Константа NOFILE означает максимальное число одновременно открытых файлов для одного процесса (это размер таблицы открытых процессом файлов, таблицы дескрипторов). Изучите описание системного вызова fcntl (file control).

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
или программно:
    /* Эта программа компилируется в start */
    /* и вызывается  вместо a.out          */
    #include <stdio.h>
    main(){
       unlink("zz.out");
       link("/dev/tty", "zz.out");
         if( !fork()){ execl("a.out", NULL); }
         else wait(NULL);
       unlink("zz.out");
    }
(про fork, exec, wait смотри в главе про UNIX).

Еще один пример: программа a.out желает запустить программу /usr/bin/vi (смотри про функцию system() сноску через несколько страниц):

    main(){
       ... system("/usr/bin/vi xx.c"); ...
    }
На вашей же машине редактор vi помещен в /usr/local/bin/vi. Тогда вы просто создаете альтернативное имя этому редактору:
    $ ln /usr/local/bin/vi /usr/bin/vi

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

    symlink(link_to_filename,link_file_name_to_be_created);

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

    char linkbuf[ MAXPATHLEN + 1]; /* куда поместить ответ */
    int len = readlink(pathname, linkbuf, sizeof linkbuf);
    linkbuf[len] = '\0';

Системный вызов stat автоматически разыменовывает символьные ссылки и выдает информацию про указуемый файл. Системный вызов lstat (аналог stat за исключением названия) выдает информацию про саму ссылку (тип файла S_IFLNK). Коды доступа к ссылке не имеют никакого значения для системы, существенны только коды доступа самого указуемого файла.

Еще раз: символьные ссылки удобны для указания файлов и каталогов на другом диске. Пусть у вас не помещается на диск каталог /opt/wawa. Вы можете разместить каталог wawa на диске USR: /usr/wawa. После чего создать символьную ссылку из /opt:

            ln -s /usr/wawa /opt/wawa
чтобы программы видели этот каталог под его прежним именем /opt/wawa.

Еще раз:

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

4.7.

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

    long offset, lseek();
       ...
    /* Узнать текущую позицию чтения/записи:
     * сдвиг на 0 от текущей позиции. lseek вернет новую
     * позицию указателя (в байтах от начала файла). */
    offset = lseek(fd, 0L, 1);  /* ftell(fp) */
А для возврата в эту точку:
       lseek(fd, offset, 0);    /* fseek(fp, offset, 0) */
По поводу lseek надо помнить такие вещи:

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 - нет! Вот кое-что про механику выделения блоков:

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

* I-узел (I-node, индексный узел) - своеобразный "паспорт", который есть у каждого файла (в том числе и каталога). В нем содержатся:

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

4.10.

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

    char c;
    while( read(0, &c, 1)) ... ; /* 0 - стандартный ввод */

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

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

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

Небезызвестная директива #include <stdio.h> включает в нашу программу файл с объявлением форматов данных и констант, используемых этой библиотекой.

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

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

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

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

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

            int c; FILE *fp = ... ;
            c = getc(fp);
getc выдает ее первый байт.

При последующих вызовах getc выдаются следующие байты из буфера, а обращений к диску уже не происходит! Лишь когда буфер будет исчерпан - произойдет очередное чтение с диска. Таким образом, информация читается из файла с опережением, заранее наполняя буфер; а по требованию выдается уже из буфера. Если мы читаем 1024 байта из файла при помощи getc(), то мы 1024 раза вызываем эту функцию, но всего 2 раза системный вызов read - для чтения двух порций информации из файла, каждая - по 512 байт.

При записи

            char c; FILE *fp = ... ;
            putc(c, fp);
выводимые символы накапливаются в буфере. Только когда в нем окажется большая порция информации, она за одно обращение write записывается на диск. Буфер записи "выталкивается" в файл в таких случаях:

Приведем упрощенную схему, поясняющую взаимоотношения основных функций и макросов из stdio (кто кого вызывает). Далее s означает строку, c - символ, fp - указатель на структуру FILE****. Функции, работающие со строками, в цикле вызывают посимвольные операции. Обратите внимание, что в конце концов все функции обращаются к системным вызовам read и write, осуществляющим ввод/вывод низкого уровня.

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

Открыть файл, создать буфер:

    #include <stdio.h>
    FILE *fp = fopen(char *name, char *rwmode);
                   |  вызывает
                   V
       int fd = open (char *name, int irwmode);
    Если открываем на запись и файл не существует (fd < 0),
    то создать файл вызовом:
           fd = creat(char *name, int accessmode);
           fd будет открыт для записи в файл.
По умолчанию fopen() использует для creat коды доступа accessmode равные 0666 (rwrw-rw-).

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

            rwmode          irwmode
            ------------------------
       "r"             O_RDONLY
            "w"             O_WRONLY|O_CREAT |O_TRUNC
            "r+"            O_RDWR
            "w+"            O_RDWR  |O_CREAT |O_TRUNC
            "a"             O_WRONLY|O_CREAT |O_APPEND
            "a+"            O_RDWR  |O_CREAT |O_APPEND

Для r, r+ файл уже должен существовать, в остальных случаях файл создается, если его не было.

Если fopen() не смог открыть (или создать) файл, он возвращает значение NULL:

    if((fp = fopen(name, rwmode)) == NULL){ ...неудача... }
Итак, схема:
    printf(fmt,...)--->--,----fprintf(fp,fmt,...)->--*
                     fp=stdout                       |
                              fputs(s,fp)--------->--|
    puts(s)----------->-------putchar(c)-----,---->--|
                                         fp=stdout   |
                      fwrite(array,size,count,fp)->--|
                                                     |
        Ядро ОС                               putc(c,fp)
    ------------------*                              |
    |файловая---<--write(fd,s,len)------------<----БУФЕР
    |система---->---read(fd,s,len)-*     _flsbuf(c,fp)
    |   |             !            |
    |системные буфера !            |
    |   |             !            V           ungetc(c,fp)
    |драйвер устр-ва  !            |                      |
    |(диск, терминал) !            |     _filbuf(fp)      |
    |   |             !            *--------->-----БУФЕР<-*
    |устройство       !                              |
    ------------------*                       c=getc(fp)
                                                     |
              rdcount=fread(array,size,count,fp)--<--|
    gets(s)-------<---------c=getchar()------,----<--|
                                         fp=stdout   |
                                                     |
                            fgets(sbuf,buflen,fp)-<--|
    scanf(fmt,.../*ук-ли*/)--<-,--fscanf(fp,fmt,...)-*
                            fp=stdin
Закрыть файл, освободить память выделенную под буфер:
    fclose(fp) ---> close(fd);
И чуть в стороне - функция позиционирования:
    fseek(fp,long_off,whence) ---> lseek(fd,long_off,whence);

Функции _flsbuf и _filbuf - внутренние для stdio, они как раз сбрасывают буфер в файл либо читают новый буфер из файла.

По указателю fp можно узнать дескриптор файла:

    int fd = fileno(fp);
Это макроопределение просто выдает поле из структуры FILE. Обратно, если мы открыли файл open-ом, мы можем ввести буферизацию этого канала:
    int fd = open(name, O_RDONLY);  /* или creat() */
            ...
    FILE *fp = fdopen(fd, "r");
(здесь надо вновь указать КАК мы открываем файл, что должно соответствовать режиму открытия open-ом). Теперь можно работать с файлом через fp, а не fd.

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

4.11.

Функция ungetc(c,fp) "возвращает" прочитанный байт в файл. На самом деле байт возвращается в буфер, поэтому эта операция неприменима к небуферизованным каналам. Возврат соответствует сдвигу указателя чтения из буфера (который увеличивается при getc()) на 1 позицию назад. Вернуть можно только один символ подряд (т.е. перед следующим ungetc-ом должен быть хоть один getc), поскольку в противном случае можно сдвинуть указатель за начало буфера и, записывая туда символ c, разрушить память программы.

    while((c = getchar()) != '+' );
    /* Прочли '+' */   ungetc(c ,stdin);
    /* А можно заменить этот символ на другой! */
    c = getchar();     /* снова прочтет '+' */

4.12.

Очень часто делают ошибку в функции fputc, путая порядок ее аргументов. Так ничего не стоит написать:

            FILE *fp = ......;
            fputc( fp, '\n' );
Запомните навсегда!
            int fputc( int c,  FILE *fp );
указатель файла идет вторым! Существует также макроопределение
            putc( c, fp );
Оно ведет себя как и функция fputc, но не может быть передано в качестве аргумента в функцию:
    #include <stdio.h>
    putNtimes(    fp,     c,     n,       f      )
            FILE *fp; int c; int n; int (*f)();
    {       while( n > 0 ){ (*f)( c, fp ); n--; }}
                 возможен вызов
            putNtimes( fp, 'a', 3, fputc );
                 но недопустимо
            putNtimes( fp, 'a', 3, putc );

Тем не менее всегда, где возможно, следует пользоваться макросом - он работает быстрее. Аналогично, есть функция fgetc(fp) и макрос getc(fp).

Отметим еще, что putchar и getchar это тоже всего лишь макросы

    #define putchar(c)      putc((c), stdout)
    #define getchar()       getc(stdin)

4.13.

Известная вам функция printf также является частью библиотеки stdio. Она входит в семейство функций:
    FILE   *fp; char bf[256];
    fprintf(fp, fmt, ... );
     printf(    fmt, ... );
    sprintf(bf, fmt, ... );

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

Для чтения данных по формату используются функции семейства

    fscanf(fp, fmt, /* адреса арг-тов */...);
     scanf(    fmt, ... );
    sscanf(bf, fmt, ... );

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

4.14.

Текстовые файлы (имеющие строчную организацию) хранятся на диске как линейные массивы байт. Для разделения строк в них используется символ '\n'. Так, например, текст

    стр1
    стрк2
    кнц
хранится как массив
    с т р 1 \n с т р к 2 \n к н ц     длина=14 байт
               !
       указатель чтения/записи (read/write pointer RWptr)
       (расстояние в байтах от начала файла)

При выводе на экран дисплея символ \n преобразуется драйвером терминалов в последовательность \r\n, которая возвращает курсор в начало строки ('\r') и опускает курсор на строку вниз ('\n'), то есть курсор переходит в начало следующей строки.

В MS DOS строки в файле на диске разделяются двумя символами \r\n и при выводе на экран никаких преобразований не делается*****. Зато библиотечные функции языка Си преобразуют эту последовательность при чтении из файла в \n, а при записи в файл превращают \n в \r\n, поскольку в Си считается, что строки разделяются только \n. Для работы с файлом без таких преобразований, его надо открывать как "бинарный":

    FILE *fp = fopen( имя, "rb" );  /* b - binary */
    int fd   = open ( имя, O_RDONLY | O_BINARY );
    '\n' - '\012' (10)  line feed
    '\r' - '\015' (13)  carriage return
    '\t' - '\011'  (9)  tab
    '\b' - '\010'  (8)  backspace
    '\f' - '\014' (12)  form feed
    '\a' - '\007'  (7)  audio bell (alert)
    '\0' - 0.           null byte

Все нетекстовые файлы в MS DOS надо открывать именно так, иначе могут произойти разные неприятности. Например, если мы программой копируем нетекстовый файл в текстовом режиме, то одиночный символ \n будет считан в программу как \n, но записан в новый файл как пара \r\n. Поэтому новый файл будет отличаться от оригинала (что для файлов с данными и программ совершенно недопустимо!).

Задание: напишите программу подсчета строк и символов в файле. Указание: надо подсчитать число символов '\n' в файле и учесть, что последняя строка файла может не иметь этого символа на конце. Поэтому если последний символ файла (тот, который вы прочитаете самым последним) не есть '\n', то добавьте к счетчику строк 1.

4.15.

Напишите программу подсчета количества вхождений каждого из символов алфавита в файл и печатающую результат в виде таблицы в 4 колонки. (Указание: заведите массив из 256 счетчиков. Для больших файлов счетчики должны быть типа long).

4.16.

Почему вводимый при помощи функций getchar() и getc(fp) символ должен описываться типом int а не char?

Ответ: функция getchar() сообщает о конце файла тем, что возвращает значение EOF (end of file), равное целому числу (-1). Это НЕ символ кодировки ASCII, поскольку getchar() может прочесть из файла любой символ кодировки (кодировка содержит символы с кодами 0...255), а специальный признак не должен совпадать ни с одним из хранимых в файле символов. Поэтому для его хранения требуется больше одного байта (нужен хотя бы еще 1 бит). Проверка на конец файла в программе обычно выглядит так:

          ...
     while((ch = getchar()) != EOF ){
          putchar(ch);
          ...
     }

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

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

    #include <stdio.h>
    #include <string.h>
    char buffer[512]; FILE *fp = ... ;
          ...
    while(fgets(buffer, sizeof buffer, fp) != NULL){
      char *sptr;
      if(sptr = strchr(buffer, '\n'))
        *sptr = '\0';
      printf("%s\n", buffer);
    }

4.18.

В чем отличие puts(s); и fputs(s,fp); ?

Ответ: puts выдает строку s в канал stdout. При этом puts выдает сначала строку s, а затем - дополнительно - символ перевода строки '\n'. Функция же fputs символ перевода строки не добавляет. Упрощенно:

    fputs(s, fp) char *s; FILE *fp;
    { while(*s) putc(*s++, fp); }
    puts(s) char *s;
    { fputs(s, stdout); putchar('\n'); }

4.19.

Найдите ошибки в программе:
      #include <stdio.h>
      main() {
          int fp;
          int i;
          char str[20];
          fp = fopen("файл");
          fgets(stdin, str, sizeof str);
          for( i = 0; i < 40; i++  );
               fputs(fp, "Текст, выводимый в файл:%s",str );
          fclose("файл");
      }
Мораль: надо быть внимательнее к формату вызова и смыслу библиотечных функций.

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

** Проверить это состояние позволяет макрос feof(fp); он истинен, если конец был достигнут, ложен - если еще нет.

*** При выполнении вызова завершения программы exit(); все открытые файлы автоматически закрываются.

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

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

4.20.

Напишите программу, которая распечатывает самую длинную строку из файла ввода и ее длину.

4.21.

Напишите программу, которая выдает n-ую строку файла. Номер строки и имя файла задаются как аргументы main().

4.22.

Напишите программу

    slice -сКакой +сколько файл
которая выдает сколько строк файла файл, начиная со строки номер сКакой (нумерация строк с единицы).
    #include <stdio.h>
    #include <ctype.h>
    long line, count, nline, ncount; /* нули */
    char buf[512];
    void main(int argc, char **argv){
      char c; FILE *fp;
      argc--; argv++;
      /* Разбор ключей */
      while((c = **argv) == '-' || c == '+'){
        long atol(), val; char *s = &(*argv)[1];
        if( isdigit(*s)){
           val = atol(s);
           if(c == '-')     nline  = val;
           else             ncount = val;
        } else fprintf(stderr,"Неизвестный ключ %s\n", s-1);
        argc--; ++argv;
      }
      if( !*argv ) fp = stdin;
      else if((fp = fopen(*argv, "r")) == NULL){
        fprintf(stderr, "Не могу читать %s\n", *argv);
        exit(1);
      }
    for(line=1, count=0; fgets(buf, sizeof buf, fp); line++){
          if(line >= nline){
             fputs(buf, stdout); count++;
          }
          if(ncount && count == ncount)
             break;
      }
      fclose(fp); /* это не обязательно писать явно */
    }
    /* End_Of_File */

4.23.

Составьте программу, которая распечатывает последние n строк файла ввода.

4.24.

Напишите программу, которая делит входной файл на файлы по n строк в каждом.

4.25.

Напишите программу, которая читает 2 файла и печатает их вперемежку: одна строка из первого файла, другая - из второго. Придумайте, как поступить, если файлы содержат разное число строк.

4.26.

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

4.27.

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

  1. Записать текст в файл.
  2. Дописать текст к концу файла.
  3. Просмотреть файл.
  4. Удалить файл.
  5. Закончить работу.

Текст вводится в файл построчно с клавиатуры. Конец ввода - EOF (т.е. CTRL/D), либо одиночный символ '.' в начале строки. Выдавайте число введенных строк.

Просмотр файла должен вестись постранично: после выдачи очередной порции строк выдавайте подсказку

    --more-- _
(курсор остается в той же строке и обозначен подчерком) и ожидайте нажатия клавиши. Ответ 'q' завершает просмотр. Если файл, который вы хотите просмотреть, не существует - выдавайте сообщение об ошибке.

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

Введите имя файла [oldfile.txt]: _

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

     system("ls -x");
а для считывания каталога в программу*
    FILE *fp = popen("ls *.c", "r");
    ... fgets(...,fp); ... // в цикле, пока не EOF
    pclose(fp);
(в этом примере читаются только имена .c файлов).

4.28.

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

4.29.

Составьте программу перекодировки текста, набитого в кодировке КОИ-8, в альтернативную кодировку и наоборот. Для этого следует составить таблицу перекодировки из 256 символов: c_new=TABLE[c_old]; Для решения обратной задачи используйте стандартную функцию strchr(). Программа читает один файл и создает новый.

4.30.

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

    #include <fcntl.h>
    #include <stdio.h>
    #define min(a,b)  (((a) < (b)) ? (a) : (b))
    #define KB                   1024  /* килобайт */
    #define PORTION         (20L* KB)  /* < 32768  */
    long    ONEFILESIZE  = (300L* KB);
    extern char    *strrchr(char *, char);
    extern long     atol   (char *);
    extern errno;           /* системный код ошибки  */
    char    buf[PORTION];   /* буфер для копирования */
    void main (int ac, char *av[]) {
        char    name[128], *s, *prog = av[0];
        int     cnt=0, done=0, fdin, fdout;
    /* M_UNIX автоматически определяется
     * компилятором в UNIX */
    #ifndef M_UNIX  /* т.е. MS DOS */
        extern int _fmode; _fmode = O_BINARY;
        /* Задает режим открытия и создания ВСЕХ файлов */
    #endif
        if(av[1] && *av[1] == '-'){ /* размер одного куска */
            ONEFILESIZE = atol(av[1]+1) * KB; av++; ac--;
        }
        if (ac < 2){
          fprintf(stderr, "Usage: %s [-size] file\n", prog);
          exit(1);
        }
        if ((fdin = open (av[1], O_RDONLY)) < 0) {
    fprintf (stderr, "Cannot read %s\n", av[1]); exit (2);
        }
        if ((s = strrchr (av[1], '.'))!= NULL) *s = '\0';
        do { unsigned long sent;
             sprintf (name, "%s.%d", av[1], ++cnt);
             if ((fdout = creat (name, 0644)) < 0) {
    fprintf (stderr, "Cannot create %s\n", name); exit (3);
             }
             sent = 0L; /* сколько байт переслано */
             for(;;){ unsigned isRead, /* прочитано read-ом */
                need = min(ONEFILESIZE - sent, PORTION);
                if( need == 0 ) break;
                sent += (isRead = read (fdin, buf, need));
                errno = 0;
                if (write (fdout, buf, isRead) != isRead &&
                    errno){ perror("write"); exit(4);
                } else if (isRead < need){ done++; break; }
             }
             if(close (fdout) < 0){
                perror("Мало места на диске"); exit(5);
             }
             printf("%s\t%lu байт\n", name, sent);
        } while( !done ); exit(0);
    }

4.31.

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

    #include <fcntl.h>
    #include <stdio.h>
    void main (int ac, char **av){
        int     i, err = 0;    FILE *fpin, *fpout;
        if (ac < 3) {
           fprintf(stderr,"Usage: %s from... to\n", av[0]);
           exit(1);
        }
        fpout = strcmp(av[ac-1], "-")  ? /* отлично от "-" */
                fopen (av[ac-1], "wb") : stdout;
        for (i = 1; i < ac-1; i++) {
            register int c;
            fprintf (stderr, "%s\n", av[i]);
            if ((fpin = fopen (av[i], "rb")) == NULL) {
                fprintf (stderr, "Cannot read %s\n", av[i]);
                err++; continue;
            }
            while ((c = getc (fpin)) != EOF)
                putc (c, fpout);
            fclose (fpin);
        }
        fclose (fpout); exit (err);
    }

Обе эти программы могут без изменений транслироваться и в MS DOS и в UNIX. UNIX просто игнорирует букву b в открытии файла "rb", "wb". При работе с read мы могли бы открывать файл как

    #ifdef M_UNIX
    # define O_BINARY       0
    #endif
    int fdin = open( av[1], O_RDONLY | O_BINARY);

4.32.

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

            способ 1        (библиотечные функции)
      #include <stdio.h>
      ...
      freopen( "имя_файла", "r", stdin );
            способ 2        (системные вызовы)
      #include <fcntl.h>
      int fd;
      ...
      fd = open( "имя_файла", O_RDONLY );
      dup2 ( fd, 0 ); /* 0 - стандартный ввод    */
      close( fd );    /* fd больше не нужен - закрыть
           его, чтоб не занимал место в таблице */
            способ 3        (системные вызовы)
      #include <fcntl.h>
      int fd;
      ...
      fd = open( "имя_файла", O_RDONLY );
      close (0);               /* 0 - стандартный ввод */
      fcntl (fd, F_DUPFD, 0 ); /* 0 - стандартный ввод */
      close (fd);
Это перенаправление ввода соответствует конструкции
    $ a.out < имя_файла

написанной на командном языке СиШелл. Для перенаправления вывода замените 0 на 1, stdin на stdout, open на creat, "r" на "w".

Рассмотрим механику работы вызова dup2**:

    new = open("файл1",...); dup2(new, old); close(new);
      таблица открытых
       файлов процесса
        ...##                    ##
    new----##---> файл1    new---##---> файл1
           ##                    ##
    old----##---> файл2    old---##     файл2
           ##                    ##
     0:до вызова     1:разрыв связи old с файл2
       dup2()     (закрытие канала old, если он был открыт)
           ##                    ##
    new----##--*--> файл1   new  ##  *----> файл1
           ##  |                 ##  |
    old----##--*            old--##--*
           ##                    ##
     2:установка old на файл1  3:после оператора close(new);
       на этом dup2 завершен.    дескриптор new закрыт.

Здесь файл1 и файл2 - связующие структуры "открытый файл" в ядре, о которых рассказывалось выше (в них содержатся указатели чтения/записи). После вызова dup2 дескрипторы new и old ссылаются на общую такую структуру и поэтому имеют один и тот же R/Wуказатель. Это означает, что в программе new и old являются синонимами и могут использоваться даже вперемежку:

    dup2(new, old);
    write(new, "a", 1);
    write(old, "b", 1);
    write(new, "c", 1);
запишет в файл1 строку "abc". Программа
    int fd;
    printf( "Hi there\n");
    fd = creat( "newout", 0640 );
    dup2(fd, 1); close(fd);
    printf( "Hey, You!\n");
выдаст первое сообщение на терминал, а второе - в файл newout, поскольку printf выдает данные в канал stdout, связанный с дескриптором 1.

4.33.

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

4.34.

Напишите программу, распечатывающую первую директиву препроцессора, встретившуюся в файле ввода.

    #include <stdio.h>
    char buf[512], word[] = "#";
    main(){  char *s; int len = strlen(word);
      while((s=fgets(buf, sizeof buf, stdin)) &&
             strncmp(s, word, len));
      fputs(s? s: "Не найдено.\n", stdout);
    }

4.35.

Напишите программу, которая переключает свой стандартный вывод в новый файл имяФайла каждый раз, когда во входном потоке встречается строка вида

            >>>имяФайла
Ответ:
    #include <stdio.h>
    char line[512];
    main(){  FILE *fp = fopen("00", "w");
       while(gets(line) != NULL)
         if( !strncmp(line, ">>>", 3)){
            if( freopen(line+3, "a", fp) == NULL){
              fprintf(stderr, "Can't write to '%s'\n", line+3);
              fp = fopen("00", "a");
            }
         } else fprintf(fp, "%s\n", line);
    }

4.36.

Библиотека буферизованного обмена stdio содержит функции, подобные некоторым системным вызовам. Вот функции - аналоги read и write:

Стандартная функция fread из библиотеки стандартных функций Си предназначена для чтения нетекстовой (как правило) информации из файла:

    int fread(addr, size, count, fp)
       register char *addr; unsigned size, count; FILE *fp;
    {  register c; unsigned ndone=0, sz;
       if(size)
         for( ; ndone < count ; ndone++){
            sz = size;
            do{   if((c = getc(fp)) >= 0 )
                        *addr++ = c;
                  else  return ndone;
            }while( --sz );
         }
       return ndone;
    }
Заметьте, что count - это не количество БАЙТ (как в read), а количество ШТУК размером size байт. Функция выдает число целиком прочитанных ею ШТУК. Существует аналогичная функция fwrite для записи в файл. Пример:
    #include <stdio.h>
    #define MAXPTS 200
    #define N      127
    char filename[] = "pts.dat";
    struct point { int x,y; } pts[MAXPTS], pp= { -1, -2};
    main(){
       int n, i;
       FILE *fp = fopen(filename, "w");
       for(i=0; i < N; i++) /* генерация точек */
          pts[i].x = i, pts[i].y = i * i;
       /* запись массива из N точек в файл */
       fwrite((char *)pts, sizeof(struct point), N, fp);
       fwrite((char *)&pp, sizeof pp,            1, fp);
       fp = freopen(filename, "r", fp);
       /* или fclose(fp); fp=fopen(filename, "r"); */
       /* чтение точек из файла в массив */
       n = fread(pts, sizeof pts[0], MAXPTS, fp);
       for(i=0; i < n; i++)
          printf("Точка #%d(%d,%d)\n",i,pts[i].x,pts[i].y);
    }

Файлы, созданные fwrite, не переносимы на машины другого типа, поскольку в них хранится не текст, а двоичные данные в формате, используемом данным процессором. Такой файл не может быть понят человеком - он не содержит изображений данных в виде текста, а содержит "сырые" байты. Поэтому чаще пользуются функциями работы с текстовыми файлами: fprintf, fscanf, fputs, fgets. Данные, хранимые в виде текста, имеют еще одно преимущество помимо переносимости: их легко при нужде подправить текстовым редактором. Зато они занимают больше места!

Аналогом системного вызова lseek служит функция fseek:

    fseek(fp, offset, whence);

Она полностью аналогична lseek, за исключением возвращаемого ею значения. Она НЕ возвращает новую позицию указателя чтения/записи! Чтобы узнать эту позицию применяется специальная функция

    long ftell(fp);

Она вносит поправку на положение указателя в буфере канала fp. fseek сбрасывает флаг "был достигнут конец файла", который проверяется макросом feof(fp);

4.37.

Найдите ошибку в программе (программа распечатывает корневой каталог в "старом" формате каталогов - с фиксированной длиной имен):
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/dir.h>
    main(){
      FILE *fp;
      struct direct d;
      char buf[DIRSIZ+1]; buf[DIRSIZ] = '\0';
      fp = fopen( '/', "r" );
      while( fread( &d, sizeof d, 1, fp) == 1 ){
        if( !d.d_ino ) continue;  /* файл стерт */
        strncpy( buf, d.d_name, DIRSIZ);
        printf( "%s\n", buf );
      }
      fclose(fp);
    }
Указание: смотри в fopen(). Внимательнее к строкам и символам! '/' и "/" - это совершенно разные вещи (хотя синтаксической ошибки нет!).

Переделайте эту программу, чтобы название каталога поступало из аргументов main (а если название не задано - используйте текущий каталог ".").

4.38.

Функциями
       fputs(    строка, fp);
      printf(    формат, ...);
     fprintf(fp, формат, ...);

невозможно вывести строку формат, содержащую в середине байт '\0', поскольку он служит для них признаком конца строки. Однако такой байт может понадобиться в файле, если мы формируем некоторые нетекстовые данные, например управляющую последовательность переключения шрифтов для принтера. Как быть? Есть много вариантов решения.

Пусть мы хотим выдать в канал fp последовательность из 4х байт "\033e\0\5". Мы можем сделать это посимвольно:

    putc('\033',fp); putc('e',   fp);
    putc('\000',fp); putc('\005',fp);
(можно просто в цикле), либо использовать один из способов:
    fprintf( fp,         "\033e%c\5", '\0');
    write  ( fileno(fp), "\033e\0\5",   4 );
    fwrite ( "\033e\0\5", sizeof(char), 4, fp);
где 4 - количество выводимых байтов.

4.39.

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

    #include <stdio.h>
    #define MAXLINES 2000  /* Максим. число строк в файле*/
    FILE *fp;              /* Указатель на файл */
    int nlines;            /* Число строк в файле */
    long offsets[MAXLINES];/* Адреса начал строк */
    extern long ftell();/*Выдает смещение от начала файла*/
    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));
    }

* - Функция

   int system(char *команда);
выполняет команду, записанную в строке команда, вызывая для этого интерпретатор команд
   /bin/sh -c "команда"

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

4.40.

Что будет выдано на экран в результате выполнения программы?
        #include <stdio.h>
        main(){
            printf( "Hello, " );
            printf( "sunny " );
            write( 1, "world", 5 );
        }

Ответ: очень хочется ответить, что будет напечатано "Hello, sunny world", поскольку printf выводит в канал stdout, связанный с дескриптором 1, а дескриптор 1 связан поумолчанию с терминалом. Увы, эта догадка верна лишь отчасти! Будет напечатано "worldHello, sunny ". Это происходит потому, что вывод при помощи функции printf буферизован, а при помощи сисвызова write - нет. printf помещает строку сначала в буфер канала stdout, затем write выдает свое сообщение непосредственно на экран, затем по окончании программы буфер выталкивается на экран.

Чтобы получить правильный эффект, следует перед write() написать вызов явного выталкивания буфера канала stdout:

            fflush( stdout );

Еще одно возможное решение - отмена буферизации канала stdout: перед первым printf можно написать

            setbuf(stdout, NULL);

Имейте в виду, что канал вывода сообщений об ошибках stderr не буферизован исходно, поэтому выдаваемые в него сообщения печатаются немедленно.

Мораль: надо быть очень осторожным при смешанном использовании буферизованного и небуферизованного обмена.

Некоторые каналы буферизуются так, что буфер выталкивается не только при заполнении, но и при поступлении символа '\n' ("построчная буферизация"). Канал stdout именно таков:

            printf("Hello\n");

печатается сразу (т.к. printf выводит в stdout и есть '\n'). Включить такой режим буферизации можно так:

            setlinebuf(fp);   или в других версиях
            setvbuf(fp, NULL, _IOLBF, BUFSIZ);
Учтите, что любое изменение способа буферизации должно быть сделано ДО первого обращения к каналу!

4.41.

Напишите программу, выдающую три звуковых сигнала. Гудок на терминале вызывается выдачей символа '\7' ('\a' по стандарту ANSI). Чтобы гудки звучали раздельно, надо делать паузу после каждого из них. (Учтите, что вывод при помощи printf() и putchar() буферизован, поэтому после выдачи каждого гудка (в буфер) надо вызывать функцию fflush() для сброса буфера).

Ответ:

    Способ 1:
            register i;
            for(i=0; i<3; i++){
               putchar( '\7' ); fflush(stdout);
               sleep(1);    /* пауза 1 сек. */
            }
    Способ 2:
            register i;
            for(i=0; i<3; i++){
               write(1, "\7", 1 );
               sleep(1);
            }

4.42.

Почему задержка не ощущается?
      printf( "Пауза...");
      sleep ( 5 );            /* ждем 5 сек. */
      printf( "продолжаем\n" );

Ответ: из-за буферизации канала stdout. Первая фраза попадает в буфер и, если он не заполнился, не выдается на экран. Дальше программа "молчаливо" ждет 5 секунд. Обе фразы будут выданы уже после задержки! Чтобы первый printf() выдал свою фразу ДО задержки, следует перед функцией sleep() вставить вызов fflush(stdout) для явного выталкивания буфера. Замечание: канал stderr не буферизован, поэтому проблему можно решить и так:

      fprintf( stderr, "Пауза..." );

4.43.

Еще один пример про буферизацию. Почему программа печатает EOF?
    #include <stdio.h>
    FILE *fwr, *frd;
    char b[40], *s; int n = 1917;
    main(){
            fwr = fopen( "aFile", "w" );
            frd = fopen( "aFile", "r" );
            fprintf( fwr, "%d: Hello, dude!", n);
            s = fgets( b, sizeof b, frd );
            printf( "%s\n", s ? s : "EOF" );
    }

Ответ: потому что к моменту чтения буфер канала fwr еще не вытолкнут в файл: файл пуст! Надо вставить

            fflush(fwr);
после fprintf(). Вот еще подобный случай:
    FILE *fp = fopen("users", "w");
           ... fprintf(fp, ...); ...
    system("sort users | uniq > 00; mv 00 users");

К моменту вызова команды сортировки буфер канала fp (точнее, последний из накопленных за время работы буферов) может быть еще не вытолкнут в файл. Следует либо закрыть файл fclose(fp) непосредственно перед вызовом system, либо вставить туда же fflush(fp);

4.44.

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

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

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

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

    #include <termio.h>
    int isatty(fd){ struct termio tt;
       return ioctl(fd, TCGETA, &tt) < 0 ? 0 : 1;
    }
    main(){
      printf("%s\n", isatty(0 /* STDIN */)? "term":"no"); }
Функция isatty является стандартной функцией*.

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

/dev/null
Это устройство, представляющее собой "черную дыру". Чтение из него немедленно выдает признак конца файла: read(...)==0; а записываемая в него информация нигде не сохраняется (пропадает). Этот файл используется, например, в том случае, когда мы хотим проигнорировать вывод какой-либо программы (сообщения об ошибках, трассировку), нигде его не сохраняя. Тогда мы просто перенаправляем ее вывод в /dev/null:
                 $ a.out > /dev/null &
Еще один пример использования:
                 $ cp /dev/hd00 /dev/null
Содержимое всего винчестера копируется "в никуда". При этом, если на диске есть сбойные блоки - система выдает на консоль сообщения об ошибках чтения. Так мы можем быстро выяснить, есть ли на диске плохие блоки.
/dev/tty
Открытие файла с таким именем в действительности открывает для нас управляющий терминал, на котором запущена данная программа; даже если ее ввод и вывод были перенаправлены в какие-то другие файлы**. Поэтому, если мы хотим выдать сообщение, которое должно появиться именно на экране, мы должны поступать так:
         #include <stdio.h>
         void message(char *s){
           FILE *fptty = fopen("/dev/tty", "w");
           fprintf(fptty, "%s\n", s);
           fclose (fptty);
         }
         main(){ message("Tear down the wall!"); }
Это устройство доступно и для записи (на экран) и для чтения (с клавиатуры).
Файлы устройств нечувствительны к флагу открытия
O_TRUNC - он не имеет для них смысла и просто игнорируется. Поэтому невозможно случайно уничтожить файл-устройство (к примеру /dev/tty) вызовом
    fd=creat("/dev/tty", 0644);

Файлы-устройства создаются вызовом mknod, а уничтожаются обычным unlink-ом. Более подробно про это - в главе "Взаимодействие с UNIX".

4.45.

Эмуляция основ библиотеки STDIO, по мотивам 4.2 BSD.
    #include <fcntl.h>
    #define BUFSIZ  512        /* стандартный размер буфера */
    #define _NFILE   20
    #define EOF    (-1)        /* признак конца файла     */
    #define NULL   ((char *) 0)
    #define IOREAD  0x0001     /* для чтения              */
    #define IOWRT   0x0002     /* для записи              */
    #define IORW    0x0004     /* для чтения и записи     */
    #define IONBF   0x0008     /* не буферизован          */
    #define IOTTY   0x0010     /* вывод на терминал       */
    #define IOALLOC 0x0020     /* выделен буфер malloc-ом */
    #define IOEOF   0x0040     /* достигнут конец файла   */
    #define IOERR   0x0080     /* ошибка чтения/записи    */
    extern char  *ttyname();
    char *tname = ttyname(fd);
Она выдаст строку, подобную "/dev/tty01". Если fd не связан с терминалом - она вернет
    extern char *malloc(); extern long lseek();
    typedef unsigned char uchar;
    uchar sibuf[BUFSIZ], sobuf[BUFSIZ];
    typedef struct _iobuf {
      int cnt;                  /* счетчик */
      uchar *ptr, *base;        /* указатель в буфер и на его начало */
      int bufsiz, flag, file;   /* размер буфера, флаги, дескриптор  */
    } FILE;
    FILE iob[_NFILE] = {
      { 0, NULL, NULL, 0, IOREAD,       0 },
      { 0, NULL, NULL, 0, IOWRT|IOTTY,  1 },
      { 0, NULL, NULL, 0, IOWRT|IONBF,  2 },
    };
    #define stdin        (&iob[0])
    #define stdout       (&iob[1])
    #define stderr       (&iob[2])
    #define putchar(c)   putc((c), stdout)
    #define getchar()    getc(stdin)
    #define fileno(fp)   ((fp)->file)
    #define feof(fp)     (((fp)->flag & IOEOF) != 0)
    #define ferror(fp)   (((fp)->flag & IOERR) != 0)
    #define clearerr(fp) ((void) ((fp)->flag &= ~(IOERR | IOEOF)))
    #define getc(fp)    (--(fp)->cnt < 0 ? \
            filbuf(fp) : (int) *(fp)->ptr++)
    #define putc(x, fp) (--(fp)->cnt < 0 ? \
            flsbuf((uchar) (x), (fp)) :    \
            (int) (*(fp)->ptr++ = (uchar) (x)))
    int fputc(int c, FILE *fp){ return putc(c, fp); }
    int fgetc(       FILE *fp){ return getc(fp);    }
    /* Открытие файла */
    FILE *fopen(char *name, char *how){
       register FILE *fp; register i, rw;
       for(fp = iob, i=0; i < _NFILE; i++, fp++)
            if(fp->flag == 0) goto found;
       return NULL; /* нет свободного слота */
    found:
       rw = how[1] == '+';
       if(*how == 'r'){
          if((fp->file = open(name, rw ? O_RDWR:O_RDONLY)) < 0)
              return NULL;
          fp->flag = IOREAD;
       } else {
          if((fp->file = open(name, (rw ? O_RDWR:O_WRONLY)| O_CREAT |
             (*how == 'a' ? O_APPEND : O_TRUNC), 0666 )) < 0)
               return NULL;
          fp->flag = IOWRT;
       }
       if(rw) fp->flag = IORW;
       fp->bufsiz = fp->cnt = 0; fp->base = fp->ptr = NULL;
       return fp;
    }
    /* Принудительный сброс буфера */
    void fflush(FILE *fp){
       uchar *base; int full= 0;
       if((fp->flag & (IONBF|IOWRT)) == IOWRT &&
          (base = fp->base) != NULL && (full=fp->ptr - base) > 0){
            fp->ptr = base; fp->cnt = fp->bufsiz;
            if(write(fileno(fp), base, full) != full)
               fp->flag |= IOERR;
       }
    }
    /* Закрытие файла */
    void fclose(FILE *fp){
       if((fp->flag & (IOREAD|IOWRT|IORW)) == 0 ) return;
       fflush(fp);
       close(fileno(fp));
       if(fp->flag  & IOALLOC) free(fp->base);
       fp->base = fp->ptr = NULL;
       fp->cnt  = fp->bufsiz = fp->flag = 0; fp->file = (-1);
    }
    /* Закрытие файлов при exit()-е */
    void _cleanup(){
       register i;
       for(i=0; i < _NFILE; i++)
           fclose(iob + i);
    }
    /* Завершить текущий процесс */
    void exit(uchar code){
       _cleanup();
       _exit(code); /* Собственно системный вызов */
    }
    /* Прочесть очередной буфер из файла */
    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);
    }
    /* Вытолкнуть очередной буфер в файл */
    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);
    }
    /* Позиционирование указателя чтения/записи */
    #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);
    }
    /* Узнать текущую позицию указателя */
    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);
    }

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

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

[Назад] [Содержание] [Вперед]