Машинно-зависимые сбои _write с кодом ошибки EINVAL

У этого есть некоторый длинный предыстория перед фактическим вопросом, однако он имеет некоторые объяснения, чтобы, надеюсь, отсеять некоторые отвлекающие факторы.

Наше приложение, разработанное на Microsoft Visual C++ (2005), использует стороннюю библиотеку (исходный код которой у нас, к счастью, есть) для экспорта сжатого файла, используемого в другом стороннем приложении. Библиотека отвечает за создание экспортированного файла, управление данными и сжатием, а также за обработку всех ошибок. Недавно мы начали получать отзывы о том, что на некоторых машинах наше приложение вылетало во время записи в файл. Основываясь на некоторых первоначальных исследованиях, мы смогли определить следующее:

  • Сбои произошли на различных настройках оборудования и операционных системах (хотя наши клиенты ограничены XP / 2000).
  • Сбои всегда происходили с одним и тем же набором данных; однако они не будут встречаться для всех наборов данных
  • Для набора данных, вызвавших сбой, сбой не воспроизводится на всех машинах, даже с аналогичными характеристиками, например, операционной системой, объемом оперативной памяти и т. Д.
  • Ошибка проявляется только тогда, когда приложение запускается в каталоге установки, а не при сборке из Visual Studio, запуске в режиме отладки или даже при запуске в других каталогах, к которым у пользователя был доступ.
  • Проблема возникает независимо от того, создается ли файл на локальном или подключенном диске.

Изучив проблему, мы обнаружили, что проблема находится в следующем блоке кода (слегка измененном для удаления некоторых макросов):

 while (size>0) {
    do {
        nbytes = _write(file->fd, buf, size);
    } while (-1==nbytes && EINTR==errno);
    if (-1==nbytes) /* error */
        throw("file write failed")
    assert(nbytes>0);
    assert((size_t)nbytes<=size);
    size -= (size_t)nbytes;
    addr += (haddr_t)nbytes;
    buf = (const char*)buf + nbytes;
}

В частности, _write возвращает код ошибки 22 или EINVAL. Согласно MSDN , _write, возвращающий EINVAL, подразумевает, что буфер (в данном случае buf) является нулевым указателем. Однако несколько простых проверок этой функции подтвердили, что это не так ни при каких обращениях к ней.

Однако мы вызываем этот метод с некоторыми очень большими наборами данных - до 250 МБ за один вызов, в зависимости от входных данных. Когда мы наложили искусственное ограничение на объем данных, передаваемых этому методу, мы, похоже, решили проблему. Это, однако, похоже на исправление кода для проблемы, которая зависит от машины / разрешений / зависит от фазы луны. Итак, теперь вопросы:

  1. Кто-нибудь знает об ограничении количества данных, которые _write может обработать за один вызов? Или - без _write - поддержка любых команд файлового ввода-вывода Visual C++?
  2. Поскольку это происходит не на всех машинах - или даже при каждом вызове достаточного размера (один вызов с 250 МБ будет работать, другой вызов - нет) - знает ли кто-либо о пользователях, машинах, настройках групповой политики или разрешениях папок, которые повлияет на это?

ОБНОВЛЕНИЕ: несколько других моментов из сообщений:

  • Мы действительно обрабатываем случаи, когда не удается выделить большой буфер. По соображениям производительности в стороннем приложении, которое читает создаваемый файл, мы хотим записать все данные в один большой блок (хотя с учетом этой ошибки это может быть невозможно).
  • Мы проверили начальное значение размера в приведенной выше процедуре, и оно совпадает с размером выделенного буфера. Кроме того, когда возникает код ошибки EINVAL, размер равен 0, а buf не является нулевым указателем, что заставляет меня думать, что это не причина проблемы.

Еще одно обновление:

Ниже приведен пример сбоя с некоторыми удобными printfs в приведенном выше примере кода.

     while (size>0) {
    if (NULL == buf)
    {
        printf("Buffer is null\n");
    }
    do {
        nbytes = _write(file->fd, buf, size);
    } while (-1==nbytes && EINTR==errno);
    if (-1==nbytes) /* error */
    {
        if (NULL == buf)
        {
            printf("Buffer is null post write\n");
        }
        printf("Error number: %d\n", errno);
        printf("Buffer address: %d\n", &buf);
        printf("Size: %d\n", size);
        throw("file write failed")
    }
    assert(nbytes>0);
    assert((size_t)nbytes<=size);
    size -= (size_t)nbytes;
    addr += (haddr_t)nbytes;
    buf = (const char*)buf + nbytes;
}

В случае сбоя это распечатает:

Error number: 22
Buffer address: 1194824
Size: 89702400

Обратите внимание, что байты не были успешно записаны и что буфер имеет действительный адрес (и проверки указателя NULL не выполнялись до или после _write)

ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ

К сожалению, мы были охвачены событиями и не смогли окончательно решить эту проблему. Нам удалось найти несколько интересных (а может быть, даже тревожных) фактов. 1. Ошибки возникали только на машинах с более медленным временем записи на жесткие диски. Два ПК с одинаковыми характеристиками оборудования, но с разными конфигурациями RAID (RAID 0 против RAID 1) будут иметь разные результаты. RAID 0 будет правильно обрабатывать данные; RAID 1 выйдет из строя. Точно так же старые ПК с более медленными жесткими дисками также выйдут из строя; более новые ПК с более быстрыми жесткими дисками, но с аналогичными процессорами / памятью, будут работать. 2. Размер записи имел значение. Когда мы ограничили объем данных, передаваемых в _write, равным 64 МБ, все файлы, кроме одного, были успешными. Когда мы ограничили его 32 МБ, все файлы были успешными.

К сожалению, я так и не получил хорошего ответа (и мы собирались позвонить в Microsoft по этому поводу, но нам пришлось заставить бизнес подписаться за счет обращения в службу технической поддержки) относительно того, почему вообще возвращался EINVAL. Это не задокументировано - насколько нам удалось найти - нигде в API библиотеки C.

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

Ответов (5)

Решение

У нас была очень похожая проблема, которую нам удалось довольно легко воспроизвести. Сначала мы скомпилировали следующую программу:

#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{ int len = 70000000;
  int handle= creat(argv[1], S_IWRITE | S_IREAD);
  setmode (handle, _O_BINARY);
  void *buf = malloc(len);
  int byteswritten = write(handle, buf, len);
  if (byteswritten == len)
    printf("Write successful.\n");
  else
    printf("Write failed.\n");
  close(handle);
  return 0;
}

Теперь предположим, что вы работаете на компьютере mycomputer и этот C: \ inbox сопоставляется с общей папкой \\ mycomputer \ inbox. Затем наблюдаем следующий эффект:

C:\>a.exe C:\inbox\x
Write successful.

C:\>a.exe \\mycomputer\inbox\x
Write failed.

Обратите внимание, что если len изменить на 60000000, проблем не будет ...

Основываясь на этой веб-странице support.microsoft.com/kb/899149 , мы думаем, что это «ограничение операционной системы» (такой же эффект наблюдается с fwrite). Наша работа состоит в том, чтобы попытаться сократить запись на части по 63 МБ, если это не удастся. Эта проблема, по-видимому, была исправлена ​​в Windows Vista.

Надеюсь, это поможет! Саймон

На ум приходят две мысли ... Либо вы проходите мимо конца буфера и пытаетесь записать эти данные, либо не удалось выделить буфер. Проблемы, которые в режиме отладки не будут так заметны, как в режиме выпуска.

Вероятно, в любом случае выделять 250 мегабайт памяти - плохая идея. Лучше выделить буфер фиксированного размера и писать фрагментами.

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

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

Поскольку большинство этих функций завершают вызов ядра WriteFile (), (или NtWriteFile ()), МОЖЕТ быть условие, что памяти ядра недостаточно для обработки буфера для записи. Но в ЭТОМ я не уверен, так как не знаю, КОГДА именно код совершает переход от UM к KM.

Не знаю, поможет ли это, но надеюсь ...

Если вы можете предоставить более подробную информацию, просьба сообщить. Иногда просто рассказав кому-то о проблеме, ваш мозг скажет: «Погодите!», И вы разберетесь в этом. хех ..

Вы можете уничтожить свой собственный стек, случайно неправильно используя указатель где-то еще - если вы можете найти репро-машину, попробуйте запустить свое приложение в Application Verifier со всеми включенными проверками памяти.

Вы смотрели на реализацию _write() в исходном коде CRT (среда выполнения C), который был установлен с Visual Studio ( C:\Program Files\Microsoft Visual Studio 8\VC\crt\src\write.c )?

Есть по крайней мере два условия , которые вызывают , _write() чтобы установить errno на EINVAL :

  1. buffer NULL, как вы упомянули.
  2. countПараметр нечетный, если файл открывается в текстовом режиме в формате UTF-16 (или UTF-8? комментарии не соответствуют коду). Это текстовый или двоичный файл? Если это текст, есть ли в нем отметка порядка байтов?
  3. Может быть, другая _write()вызывающая функция также устанавливает errnoзначение EINVAL?

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

Согласно http://msdn.microsoft.com/en-us/library/1570wh78(v=VS.90).aspx errno может принимать значения:

- EBADF
- ENOSPC
- EINVAL.

В окнах нет EINTR. Случайные системные прерывания вызывают эту ошибку и не обнаруживаются тестом. while (-1==nbytes && EINTR==errno);