Прочитать середину большого файла

Прочитать середину большого файла

У меня есть файл размером 1 ТБ. Я хотел бы прочитать его с байта 12345678901 по байт 19876543212 и поместить его на стандартный вывод на машине с оперативной памятью 100 МБ.

Я могу легко написать скрипт perl, который это делает. sysread выдает 700 МБ/с (что нормально), но syswrite выдает только 30 МБ/с. Мне бы хотелось что-то более эффективное, желательно то, что установлено в каждой системе Unix и может выдавать порядка 1 ГБ/с.

Моя первая идея:

dd if=1tb skip=12345678901 bs=1 count=$((19876543212-12345678901))

Но это неэффективно.

Редактировать:

Понятия не имею, как я неправильно измерил syswrite. Это дает 3,5 ГБ/с:

perl -e 'sysseek(STDIN,shift,0) || die; $left = shift; \
         while($read = sysread(STDIN,$buf, ($left > 32768 ? 32768 : $left))){ \
            $left -= $read; syswrite(STDOUT,$buf);
         }' 12345678901 $((19876543212-12345678901)) < bigfile

и избегает yes | dd bs=1024k count=10 | wcкошмара.

решение1

Это медленно из-за малого размера блока. Используя последнюю версию GNU dd(coreutils v8.16 +), самый простой способ — использовать параметры skip_bytesи count_bytes:

in_file=1tb

start=12345678901
end=19876543212
block_size=4096

copy_size=$(( $end - $start ))

dd if="$in_file" iflag=skip_bytes,count_bytes,fullblock bs="$block_size" \
  skip="$start" count="$copy_size"

Обновлять

fullblockопция добавлена ​​выше согласно@Жиль ответ. Сначала я думал, что это может подразумеваться под count_bytes, но это не так.

Упомянутые проблемы являются потенциальной проблемой ниже, если ddвызовы чтения/записи прерываются по какой-либо причине, то данные будут потеряны. Это маловероятно в большинстве случаев (вероятность несколько снижается, поскольку мы читаем из файла, а не из канала).


Использование a ddбез опций skip_bytesи count_bytesболее сложно:

in_file=1tb

start=12345678901
end=19876543212
block_size=4096

copy_full_size=$(( $end - $start ))
copy1_size=$(( $block_size - ($start % $block_size) ))
copy2_start=$(( $start + $copy1_size ))
copy2_skip=$(( $copy2_start / $block_size ))
copy2_blocks=$(( ($end - $copy2_start) / $block_size ))
copy3_start=$(( ($copy2_skip + $copy2_blocks) * $block_size ))
copy3_size=$(( $end - $copy3_start ))

{
  dd if="$in_file" bs=1 skip="$start" count="$copy1_size"
  dd if="$in_file" bs="$block_size" skip="$copy2_skip" count="$copy2_blocks"
  dd if="$in_file" bs=1 skip="$copy3_start" count="$copy3_size"
}

Вы также можете поэкспериментировать с разными размерами блоков, но выигрыш будет не слишком значительным. Смотрите -Есть ли способ определить оптимальное значение параметра bs для dd?

решение2

bs=1говорит ddчитать и писать по одному байту за раз. Для каждого вызова readи есть накладные расходы write, что делает это медленным. Используйте больший размер блока для приличной производительности.

Я обнаружил, что при копировании целого файла, по крайней мере в Linux,cpи catбыстрее, чемdd, даже если вы укажете большой размер блока.

Чтобы скопировать только часть файла, можно передать по конвейеру tailв head. Для этого требуется GNU coreutils или какая-то другая реализация, которая должна head -cскопировать указанное количество байтов ( tail -cесть в POSIX, но head -cне есть). Быстрый тест на Linux показывает, что это медленнее, чем dd, предположительно из-за конвейера.

tail -c $((2345678901+1)) | head -c $((19876543212-2345678901))

Проблема в ddтом, чтоэто не надежно: он может копировать частичные данные. Насколько мне известно, ddбезопасно при чтении и записи в обычный файл — см.Когда dd подходит для копирования данных? (или когда read() и write() являются частичными)- нотолько до тех пор, пока он не прерывается сигналом. С GNU coreutils вы можете использовать этот fullblockфлаг, но он непереносим.

Другая проблема заключается в ddтом, что может быть трудно найти работающее количество блоков, поскольку и количество пропущенных байтов, и количество переданных байтов должны быть кратны размеру блока. Вы можете использовать несколько вызовов dd: один для копирования первого частичного блока, один для копирования основной массы выровненных блоков и один для копирования последнего частичного блока — см.Ответ Грэмадля фрагмента оболочки. Но не забывайте, что при запуске скрипта, если только вы не используете флаг fullblock, вам нужно молиться, чтобы ddскопировать все данные. ddвозвращает ненулевой статус, если копия частичная, поэтому ошибку легко обнаружить, но нет практического способа ее исправить.

POSIX не может предложить ничего лучшего на уровне оболочки. Я бы посоветовал написать небольшую специализированную программу на языке C (в зависимости от того, что именно вы реализуете, вы можете назвать ее dd_done_rightили tail_headили mini-busybox).

решение3

С dd:

dd if=1tb skip=12345678901 count=$((19876543212-12345678901)) bs=1M iflags=skip_bytes,count_bytes

Альтернативно с losetup:

losetup --find --show --offset 12345678901 --sizelimit $((19876543212-12345678901))

И затем dd, cat... петлевое устройство.

решение4

Вот как это можно сделать:

   i=$(((t=19876543212)-(h=12345678901)))
   { dd count=0 skip=1 bs="$h"
     dd count="$((i/(b=64*1024)-1))" bs="$b"
     dd count=1 bs="$((i%b))"
   } <infile >outfile

Это все, что действительно необходимо - больше ничего не требуется. Во-первых, dd count=0 skip=1 bs=$block_size1будет lseek()обрабатывать обычный ввод файла практически мгновенно. Нет никаких шансовпропущенные данныеили какие-либо другие небылицы, которые говорят об этом, вы можете просто искать напрямую в желаемой начальной позиции. Поскольку дескриптор файла принадлежит оболочке, а dd' просто наследуют его, они повлияют на его позицию курсора, и поэтому вы можете просто сделать это пошагово. Это действительно очень просто - и нет стандартного инструмента, более подходящего для этой задачи, чем dd.

Это использует размер блока 64k, что часто является идеальным. Вопреки распространенному мнению, больший размер блока не ускоряет ddработу. С другой стороны, крошечные буферы тоже не годятся. ddнеобходимо синхронизировать свое время в системных вызовах, чтобы не приходилось ждать копирования данных в память и обратно, а также чтобы не приходилось ждать системных вызовов. Поэтому вы хотите, чтобы это заняло достаточно времени, чтобы следующему read()не пришлось ждать последнего, но не настолько, чтобы вы буферизовали большие размеры, чем необходимо.

Итак, первый ddпереходит в начальную позицию. Это занимаетнульвремя. Вы можете вызвать любую другую программу, которая вам нравится в этот момент, чтобы прочитать ее stdin, и она начнет читать прямо с нужного вам смещения байта. Я вызываю другую, ddчтобы прочитать((interval / blocksize) -1)подсчет блоков в stdout.

Последнее, что нужно, это скопировать модуль(если есть)предыдущей операции по разделению. И это все.

Не верьте, кстати, когда люди говорят факты на лицо без доказательств. Да, это возможно для того, чтобы ddсделать короткое чтение(хотя такие вещи невозможны при чтении с исправного блочного устройства - отсюда и название). Такие вещи возможны только в том случае, если вы неправильно буферизуете ddпоток, который считывается не с блочного устройства. Например:

cat data | dd bs="$num"    ### incorrect
cat data | dd ibs="$PIPE_MAX" obs="$buf_size"   ### correct

В обоих случаях ddкопиивседанных. В первом случае возможно(хотя маловероятно с cat)что некоторые из выходных блоков, которые ddкопируются, будут битами равными "$num" байтам, поскольку ddуказанотолькобуферизировать что-либо вообще, когда буфер специально запрошен в командной строке. bs=представляет собоймаксимумразмер блока, потому чтоцельddэто ввод-вывод в реальном времени.

Во втором примере я явно указываю размер выходного блока и ddбуферизую чтения, пока не будут сделаны полные записи. Это не влияет на count=то, что основано на входных блоках, но для этого вам просто нужен другой dd. Любая дезинформация, которая вам дается в противном случае, должна быть проигнорирована.

Связанный контент