Подтвердить что ты не робот

Что происходит за шторами во время ввода-вывода диска?

Когда я ищу какую-то позицию в файле и записываю небольшой объем данных (20 байтов), что происходит за кулисами?

Мое понимание

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

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

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

Мое приложение

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

Тест для предварительной реализации

Чтобы проверить, что мне не нужно придумывать какое-то блестяще переработанное решение, я проверил тест, используя несколько миллионов случайных записей, записанных в файл, который может содержать в общей сложности 200 000 записей. Каждый тест сгенерирует генератор случайных чисел с тем же значением, чтобы быть справедливым. Сначала я стираю файл и складываю его на общую длину (около 7,6 мегабайт), затем цикл несколько миллионов раз, передавая случайное смещение файла и некоторые данные в одну из двух тестовых функций:

void WriteOldSchool( void *context, long offset, Data *data )
{
    int fd = (int)context;
    lseek( fd, offset, SEEK_SET );
    write( fd, (void*)data, sizeof(Data) );
}

void WriteStandard( void *context, long offset, Data *data )
{
    FILE *fp = (FILE*)context;
    fseek( fp, offset, SEEK_SET );
    fwrite( (void*)data, sizeof(Data), 1, fp );
    fflush(fp);
}

Возможно, нет сюрпризов?

Метод OldSchool вышел сверху - много. Это было в 6 раз быстрее (1,48 миллиона против 232000 записей в секунду). Чтобы убедиться, что я не запускал аппаратное кэширование, я расширил размер своей базы данных до 20 миллионов записей (размер файла 763 мегабайта) и получил те же результаты.

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

Итак, что происходит?

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

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

Есть ли эксперты по диску, которые могут комментировать и объяснять это лучше, чем мои экспериментальные выводы? =)

4b9b3361

Ответ 1

Действительно, по крайней мере, в моей системе с GNU libc, похоже, что stdio считывает блоки 4kB, прежде чем записывать измененную часть. Мне кажется фальшивым, но я думаю, кто-то думал, что в то время это была хорошая идея.

Я проверил, записав тривиальную программу на C, чтобы открыть файл, написать небольшое количество данных один раз и выйти; затем запустил его под strace, чтобы увидеть, какие системные вызовы он фактически вызвал. Написав со смещением 10000, я увидел эти системные вызовы:

lseek(3, 8192, SEEK_SET)                = 8192
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1808) = 1808
write(3, "hello", 5)                    = 5

Кажется, вы захотите придерживаться низкоуровневого ввода-вывода Unix для этого проекта, а?

Ответ 2

Стандартные функции библиотеки C выполняют дополнительную буферизацию и, как правило, оптимизированы для потоковых чтений, а не для случайного ввода-вывода. В моей системе я не вижу ложных чтений, что Jamey Sharp видел Я вижу только ложные чтения, когда смещение не выровнено по размеру страницы - возможно, библиотека C всегда пытается сохранить его буфер IO выровнен до 4kb или что-то в этом роде.

В вашем случае, если вы делаете много случайных чтений и записи по достаточно маленькому набору данных, вам, скорее всего, лучше всего будет пользоваться pread/pwrite, чтобы избежать необходимости искать системные вызовы или просто mmap установить набор данных и записать его в память (скорее всего, это самый быстрый, если ваш набор данных подходит в памяти).