Надежный способ определить размер файла в POSIX/OS X с учетом файлового дескриптора.

Я написал функцию для наблюдения за тем, как файл (с учетом fd) увеличивается до определенного размера, включая тайм-аут. Я использую kqueue()/kevent(), чтобы дождаться «расширения» файла, но после того, как я получаю уведомление о том, что файл увеличился, мне нужно проверить размер файла (и сравнить его с желаемым размером). Это кажется простым, но я не могу найти способ сделать это надежно в POSIX.

NB: тайм-аут сработает, если файл вообще не увеличится в течение указанного времени. Таким образом, это не абсолютный тайм-аут, а просто тайм-аут, когда файл увеличивается. Я работаю в OS X, но этот вопрос предназначен для «каждого POSIX, имеющего kevent()/kqueue()», который, я думаю, должен быть OS X и BSD.

Вот моя текущая версия моей функции:

/**
 * Blocks until `fd` reaches `size`. Times out if `fd` isn't extended for `timeout`
 * amount of time. Returns `-1` and sets `errno` to `EFBIG` should the file be bigger
 * than wanted.
 */
int fwait_file_size(int fd,
                    off_t size,
                    const struct timespec *restrict timeout)
{
    int ret = -1;
    int kq = kqueue();
    struct kevent changelist[1];

    if (kq < 0) {
        /* errno set by kqueue */
        ret = -1;
        goto out;
    }

    memset(changelist, 0, sizeof(changelist));
    EV_SET(&changelist[0], fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, NOTE_DELETE | NOTE_RENAME | NOTE_EXTEND, 0, 0);

    if (kevent(kq, changelist, 1, NULL, 0, NULL) < 0) {
        /* errno set by kevent */
        ret = -1;
        goto out;
    }

    while (true) {
        {
            /* Step 1: Check the size */
            int suc_sz = evaluate_fd_size(fd, size); /* IMPLEMENTATION OF THIS IS THE QUESTION */
            if (suc_sz > 0) {
                /* wanted size */
                ret = 0;
                goto out;
            } else if (suc_sz < 0) {
                /* errno and return code already set */
                ret = -1;
                goto out;
            }
        }

        {
            /* Step 2: Wait for growth */
            int suc_kev = kevent(kq, NULL, 0, changelist, 1, timeout);

            if (0 == suc_kev) {
                /* That's a timeout */
                errno = ETIMEDOUT;
                ret = -1;
                goto out;
            } else if (suc_kev > 0) {
                if (changelist[0].filter == EVFILT_VNODE) {
                    if (changelist[0].fflags & NOTE_RENAME || changelist[0].fflags & NOTE_DELETE) {
                        /* file was deleted, renamed, ... */
                        errno = ENOENT;
                        ret = -1;
                        goto out;
                    }
                }
            } else {
                /* errno set by kevent */
                ret = -1;
                goto out;
            }
        }
    }

    out: {
        int errno_save = errno;
        if (kq >= 0) {
            close(kq);
        }
        errno = errno_save;
        return ret;
    }
}

Таким образом, основной алгоритм работает следующим образом:

  1. Настроить кевент
  2. Проверить размер
  3. Дождитесь роста файла

Шаги 2 и 3 повторяются до тех пор, пока файл не достигнет нужного размера.

В коде используется функция int evaluate_fd_size(int fd, off_t wanted_size), которая возвращает < 0, если "произошла какая-то ошибка или файл больше требуемого", == 0, если "файл еще недостаточно велик", или > 0, если файл достиг нужного размера.

Очевидно, это работает только в том случае, если evaluate_fd_size надежно определяет размер файла. Первым делом я реализовал это с помощью off_t eof_pos = lseek(fd, 0, SEEK_END) и сравнил eof_pos с wanted_size. К сожалению, lseek, похоже, кэширует результаты. Так что даже когда kevent вернулся с NOTE_EXTEND, так что файл вырос, результат может быть тот же! Потом я решил переключиться на fstat, но обнаружил статьи, которые fstat тоже кэширует.

Последнее, что я пробовал, это использовать fsync(fd); перед off_t eof_pos = lseek(fd, 0, SEEK_END);, и вдруг все заработало. Но:

  1. Ничто не говорит о том, что fsync() действительно решает мою проблему
  2. Я не хочу fsync() из-за производительности

EDIT: это действительно сложно воспроизвести, но я видел один случай, когда fsync() не помогло. Кажется, требуется (очень мало) времени, пока размер файла не станет больше после того, как событие NOTE_EXTEND попадет в пространство пользователя. fsync(), вероятно, просто работает как достаточно хороший sleep() и поэтому работает большую часть времени :-.

Итак, другими словами: как надежно проверить размер файла в POSIX, не открывая/закрывая файл, что я не могу сделать, потому что не знаю имени файла. Кроме того, я не могу найти гарантии, что это поможет

Кстати: int new_fd = dup(fd); off_t eof_pos = lseek(new_fd, 0, SEEK_END); close(new_fd); проблему с кешированием не поборол.

РЕДАКТИРОВАНИЕ 2: я также создал все в одном демонстрационная программа. Если он печатает Ok, success перед выходом, все прошло нормально. Но обычно он печатает Timeout (10000000), что свидетельствует о состоянии гонки: проверка размера файла для последнего запущенного kevent меньше фактического размера файла в данный момент. Как ни странно, при использовании ftruncate() для увеличения файла вместо write() кажется, что это работает (вы можете скомпилировать тестовую программу с -DUSE_FTRUNCATE, чтобы проверить это).


person Johannes Weiss    schedule 09.12.2013    source источник
comment
Можете ли вы попробовать выполнить fchown(fd, -1, -1) перед вызовом stat ?   -  person cnicutar    schedule 09.12.2013
comment
@cnicutar Пробовал, не помогло :-(   -  person Johannes Weiss    schedule 09.12.2013


Ответы (1)


  1. Ничто не говорит о том, что fsync() действительно решает мою проблему
  2. Я не хочу использовать fsync() из-за производительности

Ваша проблема не в «результатах кэширования fstat», а в буферизации системы ввода-вывода. Fstat не обновляется до тех пор, пока ядро ​​не сбросит буферы ввода-вывода в базовую файловую систему.

Вот почему fsync решает вашу проблему, и любое решение вашей проблемы более или менее должно делать эквивалент fsync. (Это то, что решение open/close делает как побочный эффект.)

Не могу помочь вам с 2, потому что не вижу способа избежать использования fsync.

person Fred the Magic Wonder Dog    schedule 09.12.2013
comment
Отредактировал мой вопрос, fsync() видимо не решает проблему полностью. Я видел один случай, когда этого было недостаточно. Я предполагаю, что это просто приостанавливает выполнение программы на некоторое время и, следовательно, уменьшает количество случаев, когда я могу наблюдать состояние гонки. - person Johannes Weiss; 10.12.2013