В задачах этой главы появилась некоторая системозависимость. Моя рабочая система: 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 -- нет. Проверяем:
Перейдем к реализации 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 сделаем такие замены:
- Условие внутри цикла for
if (fp->flag.is_read == 0 && fp->flag.is_write == 0)
break; /* найдена свободная позиция */
- Установка флага 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.
Разберёмся с тем, что делает каждая из этих функций.
- _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 оказался велик для моих тестовых файлов.
- 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;
}
- 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