Date Редакция Категория comp Теги C / K&R

В задачах этой главы появилась некоторая системозависимость. Моя рабочая система: Xubuntu 12.04, компилятор gcc версия 4.6.3.

Упражнение 8.1. Перепишите программу cat из главы 7, используя функции read, write, open и close. Замените ими соответствующие функции стандартной библиотеки. Поэкспериментируйте, чтобы сравнить быстродействие двух версий.

Самое интересное в задании было: узнавать, где что лежит. Так, unistd.h обеспечивает доступ к API POSIX-совместимой операционной системы (close, read, write). fcntl.h содержит функции и константы, управляющие доступом к файлам (open, O_RDONLY).

 #include <stdio.h>
 #include <stdlib.h>
 #include <fcntl.h>
 #include <unistd.h>

 #define PERMS 0666     /* RW для собственника, группы и остальных */

 /* cat: конкатенация файлов (использование системных вызовов)  */
 int main(int argc, char *argv[])
 {
    int fd;
    void filecopy(int, int);
    char *prog = argv[0]; /* имя программы */

    if (argc == 1)        /* нет аргументов, копируется станд. ввод */
        filecopy(0, 1);
    else
        while (--argc > 0)
            if ((fd = open(*++argv, O_RDONLY, PERMS)) == -1) {
                fprintf(stderr, "%s: не могу открыть файл %s\n", prog, *argv);
                exit(1);
            } else {
                filecopy(fd, 1);
                close(fd);
            }
    if (ferror(stdout)) {
        fprintf(stderr, "%s: ошибка записи в stdout\n", prog);
        exit(2);
    }
    exit(0);
 }

 /* filecopy: копирует файл ifd в файл ofd */
 void filecopy(int ifd, int ofd)
 {
    char buf[BUFSIZ];
    int n;

    while ((n = read(ifd, buf, BUFSIZ)) > 0)
        write(ofd, buf, n);
 }

Упражнение 8.2. Перепишите функции fopen и _fillbuf, работая с флажками как с полями, а не с помощью явных побитовых операций. Сравните размеры и скорости двух вариантов программ.

Перед тем как решать задачу захотелось реализовать вариант, предложенный авторами. В п. 8.5 K&R приведен фрагмент stdio.h. Чтобы не переопределять библиотечные переменные и определения, добавил везде приставку "my_". Наша программа пытается открыть с помощью fopen некоторый файл и сообщает об успехе или жалуется на невозможность такого открытия.

 #include <stdio.h>
 #include <stdlib.h>
 #include <fcntl.h>
 #include <unistd.h>

 #define OPEN_MAX 20 /* max число одновременно открытых файлов */
 #define PERMS 0666 /* RW для собственника, группы и проч. */

 typedef struct _my_iobuf {
    int cnt;        /* количество оставшихся символов */
    char *ptr;      /* позиция следующего символа */
    char *base;     /* адрес буфера */
    int flag;       /* режим  доступа */
    int fd;         /* дескриптор файла */
 } MY_FILE;

 enum _flags {
    _READ  = 01,  /* файл открыт на чтение */
    _WRITE = 02,  /* файл открыт на запись */
    _UNBUF = 04,  /* файл не буферизируется */
    _EOF   = 010, /* в данном файле встретился EOF */
    _ERR   = 020  /* в данном файле встретилась ошибка */
 };

 MY_FILE _my_iob[OPEN_MAX];
 MY_FILE *my_fopen(char *, char *);

 int main(int argc, char *argv[])
 {
    MY_FILE *fp;
    char *prog = argv[0]; /* имя программы */

    while (--argc > 0) {
        if ((fp = my_fopen(*++argv, "r")) == NULL) {
            fprintf(stderr, "%s: не могу открыть файл %s\n", prog, *argv);
            exit(1);
        }
        else {
            fprintf(stdout, "%s: открыт файл %s\n", prog, *argv);
        }
    }
    exit(0);
 }

 /* my_fopen: открывает файл, возвращает файловый указатель */
 MY_FILE *my_fopen(char *name, char *mode)
 {
    int fd;
    MY_FILE *fp;

    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    for (fp = _my_iob; fp < _my_iob + OPEN_MAX; fp++)
        if ((fp->flag & (_READ | _WRITE)) == 0)
            break;              /* найдена свободная позиция */
    if (fp >= _my_iob + OPEN_MAX)  /* нет свободной позиция */
        return NULL;

    if (*mode == 'w')
        fd = creat(name, PERMS);
    else if (*mode == 'a') {
        if ((fd = open(name, O_WRONLY, 0)) == -1)
            fd = creat(name, PERMS);
        lseek(fd, 0L, 2);
    } else
        fd = open(name, O_RDONLY, 0);
    if (fd ==-1) /* невозможен доступ по имени name */
        return NULL;
    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    fp->flag = (*mode == 'r') ? _READ : _WRITE;
    return fp;
 }

Файл test1.txt существует, а test4.txt -- нет. Проверяем:

fopen0.png

Перейдем к реализации fopen (точнее, my_fopen) с битовыми полями. Для этого в определении типа MY_FILE поле flag нужно сделать битовым полем (flags):

 struct flags {
    unsigned int is_read  : 1; /* файл открыт на чтение */
    unsigned int is_write : 1; /* файл открыт на запись */
    unsigned int is_unbuf : 1; /* файл не буферизируется */
    unsigned int is_eof   : 1; /* в данном файле встретился EOF */
    unsigned int is_err   : 1; /* в данном файле встретилась ошибка */
 };

 typedef struct _my_iobuf {
    int cnt;            /* количество оставшихся символов */
    char *ptr;          /* позиция следующего символа */
    char *base;         /* адрес буфера */
    struct flags flag; /* режим  доступа */
    int fd;             /* дескриптор файла */
 } MY_FILE;

В самой функции fopen сделаем такие замены:

  1. Условие внутри цикла for
 if (fp->flag.is_read == 0 && fp->flag.is_write == 0)
     break;              /* найдена свободная позиция */
  1. Установка флага flag, в зависимости от режима работы с файлом
 if (*mode == 'r') {
    fp->flag.is_read  = 1;
    fp->flag.is_write = 0;
 }
 else {
    fp->flag.is_read  = 0;
    fp->flag.is_write = 1;
 }

Теперь всё вместе:

 #include <stdio.h>
 #include <stdlib.h>
 #include <fcntl.h>
 #include <unistd.h>

 #define OPEN_MAX 20 /* max число одновременно открытых файлов */
 #define PERMS 0666 /* RW для собственника, группы и проч. */

 struct flags {
    unsigned int is_read  : 1; /* файл открыт на чтение */
    unsigned int is_write : 1; /* файл открыт на запись */
    unsigned int is_unbuf : 1; /* файл не буферизируется */
    unsigned int is_eof   : 1; /* в данном файле встретился EOF */
    unsigned int is_err   : 1; /* в данном файле встретилась ошибка */
 };

 typedef struct _my_iobuf {
    int cnt;            /* количество оставшихся символов */
    char *ptr;          /* позиция следующего символа */
    char *base;         /* адрес буфера */
    struct flags flag; /* режим  доступа */
    int fd;             /* дескриптор файла */
 } MY_FILE;

 MY_FILE _my_iob[OPEN_MAX];
 MY_FILE *my_fopen(char *, char *);

 int main(int argc, char *argv[])
 {
    MY_FILE *fp;
    char *prog = argv[0]; /* имя программы */

    while (--argc > 0) {
        if ((fp = my_fopen(*++argv, "r")) == NULL) {
            fprintf(stderr, "%s: не могу открыть файл %s\n", prog, *argv);
            exit(1);
        }
        else {
            fprintf(stdout, "%s: открыт файл %s\n", prog, *argv);
        }
    }
    exit(0);
 }

 /* my_fopen: открывает файл, возвращает файловый указатель */
 MY_FILE *my_fopen(char *name, char *mode)
 {
    int fd;
    MY_FILE *fp;

    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    for (fp = _my_iob; fp < _my_iob + OPEN_MAX; fp++)
        if (fp->flag.is_read == 0 && fp->flag.is_write == 0)
            break;              /* найдена свободная позиция */
    if (fp >= _my_iob + OPEN_MAX)  /* нет свободной позиция */
        return NULL;

    if (*mode == 'w')
        fd = creat(name, PERMS);
    else if (*mode == 'a') {
        if ((fd = open(name, O_WRONLY, 0)) == -1)
            fd = creat(name, PERMS);
        lseek(fd, 0L, 2);
    } else
        fd = open(name, O_RDONLY, 0);
    if (fd ==-1) /* невозможен доступ по имени name */
        return NULL;
    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    if (*mode == 'r') {
        fp->flag.is_read  = 1;
        fp->flag.is_write = 0;
    }
    else {
        fp->flag.is_read  = 0;
        fp->flag.is_write = 1;
    }
    return fp;
 }

Функция _fillbuf() переделывается аналогично. Полный код программы здесь.

 /* _fillbuf: запрос памяти и заполнение буфера */
 int _fillbuf(MY_FILE *fp)
 {
    int bufsize;

    if (fp->flag.is_read == 0 || 
        fp->flag.is_eof == 1 || 
        fp->flag.is_err == 1)
        return EOF;
    bufsize = (fp->flag.is_unbuf == 1) ? 1 : BUFSIZ;
    if (fp->base == NULL) /* буфера еще нет */
       if ((fp->base = (char *) malloc(bufsize)) == NULL)
           return EOF; /* нельзя получить буфер */
    fp->ptr = fp->base;
    fp->cnt = read(fp->fd, fp->ptr, bufsize);
    if (--fp->cnt < 0) {
        if (fp->cnt == -1)
            fp->flag.is_eof = 1;
        else
            fp->flag.is_err = 1;
        fp->cnt = 0;
        return EOF;
    }
    return (unsigned char) *fp->ptr++;
 }

Что же касается скорости, то, по идее, реализация битовых полей машиннозависима и должны быть медленнее.

Упражнение 8.3. Разработайте и напишите функции _flushbuf, fflush и fclose.

Разберёмся с тем, что делает каждая из этих функций.

  1. _flushbuf(). Здесь у меня возникли проблемы: я не понимал почему putc (см. пример stdio.h из п. 8.5) реализован именно так:
 #define putc(x,p) (--(p)->cnt >= 0 \ 
               ? *(p)->ptr++ = (x) : _flushbuf((x),p))

Соответственно, не понимал, что делает _flushbuf.

Всему "виной" моя тупостьдвойственный смысл cnt. В getc он показывает, сколько ещё можно считать, а в putc -- сколько ещё можно записать. Затем, когда записывать уже некуда, буфер очищается.

Построим _flushbuf() по образцу _fillbuf(), т. е.

  • Если нет разрешения на запись в буфер, то выдаём EOF (в _fillbuf проверялось разрешение на чтение).
  • Если буфера нет, то он выделяется (как в _fillbuf),
  • если же буфер есть -- записываем в него (в _fillbuf -- считываем).
  • Сохраняем аргумент в буфере, изменяя соответственно cnt.

Итог:

 /* _flushbuf: запрос памяти и очистка буфера */
 int _flushbuf(int x, MY_FILE *fp)
 {
    int bufsize, numchar;

    if (fp->flag.is_write == 0 || 
        fp->flag.is_eof == 1 || 
        fp->flag.is_err == 1)
        return EOF;
    bufsize = (fp->flag.is_unbuf == 1) ? 1 : BUFSIZ;
    if (fp->base == NULL) { /* буфера еще нет */
        if ((fp->base = (char *) malloc(bufsize)) == NULL) {
            fp->flag.is_err = 1;
            return EOF; /* нельзя получить буфер */
        }
    }
    else {
        numchar = fp->ptr - fp->base;
        if (write(fp->fd, fp->base, numchar) != numchar) {
           fp->flag.is_err = 1;
           return EOF; /* ошибка записи */
       }
    }
    fp->ptr = fp->base;
    *fp->ptr++ = (unsigned char) x;    
    fp->cnt = bufsize - 1;
    if (--fp->cnt < 0) {
        if (fp->cnt == -1)
            fp->flag.is_eof = 1;
        else
            fp->flag.is_err = 1;
        return EOF;
    }
    return x;
 }

Скачать код. При проверке _flushbuf пришлось использовать маленький размер буфера, т.к. системный BUFSIZ = 8192 оказался велик для моих тестовых файлов.

  1. fflush() -- очистка буфера, соответствующего открытому файлу, представляет собой надстройку над _flushbuf. По изящной метафоре, прочитанной где-то в сети, работа fflush напоминает кнопку унитаза: нажал (вызвал) её, и бачок (буфер) опустел. Нам нужно очистить буфер и установить начальные значения указателя на следующий символ и счетчика не записанных символов.
 /* fflush: очистка буфера, соответствующего файлу fp */
 int my_fflush(MY_FILE *fp)
 {
    int ret = 0;

    if (fp->flag.is_write)
        ret = _flushbuf(0, fp);
    fp->ptr = fp->base;
    fp->cnt = (fp->flag.is_unbuf == 1) ? 1 : BUFSIZ;
    return ret;
 }
  1. fclose(). Если в закрываемый файл что-то записывалось, то сбросить это в него (с помощью fflush) и установить начальные значения элементов структуры _iobuf (_my_iobuf).
 /* fclose: закрыть файл fp */
 int my_fclose(MY_FILE *fp)
 {
    int ret;

    if ((ret = my_fflush(fp)) != EOF) {
        free (fp->base);
        fp->ptr = NULL;
        fp->base = NULL;
        fp->cnt = 0;
        fp->flag.is_read = 0; 
        fp->flag.is_write = 0;
    }
    return ret;
 }

Упражнение 8.4. Функция стандартной библиотеки

 int fseek(FILE *fp, long offset, int origin) 

идентична функции lseek с теми, однако, отличиями, что fp -- это файловый указатель, а не дескриптор, и возвращает она значение int, означающее состояние файла, а не позицию в нем. Напишите свою версию fseek. Обеспечьте, чтобы работа вашей fseek по буферизации была согласована с буферизацией, используемой другими функциями библиотеки.

Внутри нашей fseek находится lseek, которая и реализует смещение. Файл fp может быть открыт на чтение или на запись. Если он открыт на чтение и смещение вычисляется относительно текущей позиции считывания (origin = 1), но нужно учесть символы, которые к этому моменту находятся в буфере. Если же файл открыт на запись, то нужно сначала записать то, что находится в буфере, а затем использовать lseek.

fseek возвращает 0, если всё в порядке, и -1 в случае ошибки.

 /* my_fseek: смещает текущую позицию в fp на offset относительно origin */
 int my_fseek(MY_FILE *fp, long offset, int origin)
 {
    int ret, numchar;

    if (fp->flag.is_read) {
        if (origin == 1)
            offset -= fp->cnt;
        lseek(fp->fd, offset, origin);
        fp->cnt = 0;
    }
    else if (fp->flag.is_write) {
        if ((numchar = fp->ptr - fp->base) > 0)
            if (write(fp->fd, fp->base, numchar) != numchar) {
                ret = -1;
        if (ret != -1) /* если нет ошибок */
            lseek(fp->fd, offset, origin);
       }
    }
    return ret == -1 ? - 1 : 0;
 }

Упражнение 8.5. Модифицируйте fsize таким образом, чтобы можно было печатать остальную информацию, содержащуюся в узле inode.

Здесь, уже не впервые в этой главе, реализовать пример из К&R оказалось интереснее результата. Основываясь на том, что opendir, readdir и closedir -- системно-зависимые, я решил не пытаться реализовывать их, а просто подключить одноименные функции, имеющиеся в моей системе. Таким образом, у меня остались, помимо основной функции, fsize и dirwalk. Код здесь.

Всё, что нам требуется модифицировать -- вывод printf в fsize. Будем выводить, помимо размера файла, номер inode и время последней модификации (код):

 void fsize(char *name)
 {
    struct stat stbuf;
    struct tm * time_info;
    char timestr[5];  /* "HH:MM\0" */

    if (stat(name, &stbuf) == -1) {
        fprintf(stderr, "fsize: нет доступа к %s\n", name);
        return;
    }
    if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
        dirwalk(name, fsize);

    time_info = localtime(&stbuf.st_mtime);    
    strftime(timestr, sizeof(stbuf.st_mtim), "%H:%M", time_info);
    printf("%ld %8ld %s %s\n", stbuf.st_ino, stbuf.st_size, timestr, name);
 }

Упражнение 8.6. Стандартная функция calloc(n, size) возвращает указатель на n элементов памяти размера size, заполненных нулями. Напишите свой вариант calloc, пользуясь функцией malloc или модифицируя последнюю.

Запустим вначале базовую программу из K&R -- код.

Текст my_calloc():

 /* calloc: распределитель памяти под nobj объектов размера size */
 void *my_calloc(unsigned nobj, unsigned size)
 {
    unsigned nbytes, i;
    char *p, *q;

    nbytes = nobj * size; /* общее число байтов, которое нужно выделить */
    if ((p = q = my_malloc(nbytes)) != NULL)
        for (i = 0; i < nbytes; i++)
            *p++ = 0;
    return q;
 }

Код был бы ещё короче, если бы не необходимость инициализации нулями. А так -- если память выделить можно, то заполняем nbytes нулями.

Код полностью.

Упражнение 8.7. Функция malloc допускает любой размер, никак не проверяя его на правдоподобие: free предполагает, что размер освобождаемого блока -- правильный. Усовершенствуйте эти программы таким образом, чтобы они более тщательно контролировали ошибки.

Введем константу MAXBYTES, соответствующую максимальному числу байт, которое можно запросить у системы. Если в malloc мы запросим больше, получим сообщение об ошибке.

 #define MAXBYTES 10240 /* макс. число байт для запроса */

 /* malloc: универсальный распределитель памяти с проверкой */
 void *my_malloc(unsigned nbytes)
 {
    Header *p, *prevp;
    Header *morecore(unsigned);
    unsigned nunits;

    if (nbytes > MAXBYTES) {
        printf("error: превышен максимальный размер запроса %d\n", MAXBYTES);
        return NULL;
    }

    //... как и раньше
 }

В статической переменной maxallocsize сохраним размер максимального из выделенных блоков. free, следовательно, не может освобождать блоки, бОльшие maxallocsize (и равные или меньшие нулю).

В результате, в morecore изменилась одна строка

   maxallocsize = ((up->s.size = nu) > maxallocsize) ? up->s.size : maxallocsize;

и получаем:

 static unsigned maxallocsize;  /* максимальный размер выделенного блока */

 /* morecore: запрашивает у системы дополнительную память */ 
 static Header * my_morecore(unsigned nu)
 {
    char *cp;
    Header *up;

    if (nu < NALLOC)
        nu = NALLOC;
    cp = sbrk(nu * sizeof(Header));
    if (cp == (char *) -1) /* больше памяти нет. */
        return NULL;
    up = (Header *) cp;
    maxallocsize = ((up->s.size = nu) > maxallocsize) ? up->s.size : maxallocsize;
    my_free((void *)(up+1));
    return freep;
 }

А в free добавилась проверка:

 /* free: включает блок в список свободной памяти */
 void my_free(void *ap)
 {
    Header *bp, *p;

    bp = (Header *) ap -1; /* указатель на заголовок блока */

    if (bp->s.size <= 0 || bp->s.size > maxallocsize) {
        printf("error: не могу освободить блок размером %u байт \n", 
                                                          maxallocsize);
        exit(1);
    }

    //... как и раньше

 }

Код полностью.

Упражнение 8.8. Напишите программу bfree(p, n), освобождающую произвольный блок p, состоящий из n символов, путем включения его в список свободной памяти, поддерживаемый функциями malloc и free. С помощью bfree пользователь должен иметь возможность в любое время добавить в список свободной памяти статический или внешний массив.

Под произвольным блоком понимается, по-видимому, что размер блока не обязательно кратен размеру заголовка. Поскольку указатель на блок уже является входным аргументом bfree, то пусть эта функция возвращает размер выделенного блока в sizeof(Header) или 0, если выделить блок не удаётся.

Код:

 unsigned bfree(char *p, unsigned n)
 {
    Header *bp;

    if (n < sizeof(Header))
        return 0;
    bp = (Header *) p;
    bp->s.size = n / sizeof(Header);
    my_free((void *)(bp+1));
    return bp->s.size;
 }

Voila!



Комментарии

comments powered by Disqus