Lire le milieu d'un gros fichier

19

J'ai un fichier de 1 To. Je voudrais lire de l'octet 12345678901 à l'octet 19876543212 et mettre cela sur la sortie standard sur une machine avec 100 Mo de RAM.

Je peux facilement écrire un script perl qui fait cela. sysread délivre 700 Mo / s (ce qui est bien), mais syswrite ne délivre que 30 Mo / s. Je voudrais quelque chose de plus efficace, de préférence quelque chose qui est installé sur chaque système Unix et qui peut fournir de l'ordre de 1 Go / s.

Ma première idée est:

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

Mais ce n'est pas efficace.

Éditer:

Je n'ai aucune idée de la façon dont j'ai mal mesuré l'écriture système. Cela fournit 3,5 Go / s:

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

et évite le yes | dd bs=1024k count=10 | wccauchemar.

Ole Tange
la source
votre commande avecbs=1M iflag=skip_bytes,count_bytes
frostschutz

Réponses:

21

Ceci est lent en raison de la petite taille du bloc. En utilisant un GNU récent dd( coreutils v8.16 + ), la manière la plus simple est d'utiliser les options skip_byteset 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"

Mise à jour

fullblockoption ajoutée ci-dessus selon la réponse @Gilles . Au début, je pensais que cela pouvait être impliqué par count_bytes, mais ce n'est pas le cas.

Les problèmes mentionnés sont un problème potentiel ci-dessous, si ddles appels en lecture / écriture sont interrompus pour une raison quelconque, les données seront perdues. Ce n'est pas probable dans la plupart des cas (les chances sont quelque peu réduites puisque nous lisons à partir d'un fichier et non d'un tuyau).


L'utilisation d'un ddsans les options skip_byteset count_bytesest plus difficile:

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"
}

Vous pouvez également expérimenter différentes tailles de blocs, mais les gains ne seront pas très spectaculaires. Voir - Existe - t-il un moyen de déterminer la valeur optimale pour le paramètre bs à dd?

Graeme
la source
@Graeme ne sera pas la deuxième méthode échouer si ce bsn'est pas un facteur de skip?
Steven Penny
@StevenPenny, je ne sais pas où vous voulez en venir, mais skipc'est un certain nombre de blocs, pas d'octets. Peut-être que vous êtes confus car skip_bytesest utilisé dans le premier exemple, ce qui signifie qu'il skip est en octets?
Graeme
Votre bsest 4,096, ce qui signifie que vous ne pouvez pas sauter plus précisément que les 4,096octets
Steven Penny
1
@StevenPenny, c'est pourquoi il existe trois exécutions différentes de ddla première et de la dernière utilisation bs=1afin de copier les données qui ne démarrent ni ne se terminent sur un alignement de bloc.
Graeme
6

bs=1indique ddde lire et d'écrire un octet à la fois. Il y a des frais généraux pour chacun readet pour chaque writeappel, ce qui rend cela lent. Utilisez une taille de bloc plus grande pour des performances décentes.

Lorsque vous copiez un fichier entier, au moins sous Linux, je l'ai trouvé cpet catsont plus rapides quedd , même si vous spécifiez une grande taille de bloc.

Pour copier uniquement une partie d'un fichier, vous pouvez diriger tailvers head. Cela nécessite GNU coreutils ou une autre implémentation qui doit head -ccopier un nombre spécifié d'octets ( tail -cest dans POSIX mais head -cne l'est pas). Un benchmark rapide sur Linux montre que cela est plus lent que dd, probablement à cause du tuyau.

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

Le problème ddest qu'il n'est pas fiable: il peut copier des données partielles . Pour autant que je sache, ddest-il sûr lors de la lecture et de l'écriture dans un fichier normal - voir Quand dd convient-il pour copier des données? (ou, quand sont read () et write () partial) - mais seulement tant qu'il n'est pas interrompu par un signal . Avec GNU coreutils, vous pouvez utiliser l' fullblockindicateur, mais ce n'est pas portable.

Un autre problème ddest qu'il peut être difficile de trouver un nombre de blocs qui fonctionne, car le nombre d'octets ignorés et le nombre d'octets transférés doivent être un multiple de la taille du bloc. Vous pouvez utiliser plusieurs appels pour dd: un pour copier le premier bloc partiel, un pour copier la masse des blocs alignés et un pour copier le dernier bloc partiel - voir la réponse de Graeme pour un extrait de shell. Mais n'oubliez pas que lorsque vous exécutez le script, à moins que vous n'utilisiez le fullblockdrapeau, vous devez prier pour ddcopier toutes les données. ddrenvoie un état différent de zéro si une copie est partielle, il est donc facile de détecter l'erreur, mais il n'y a aucun moyen pratique de la réparer.

POSIX n'a ​​rien de mieux à offrir au niveau du shell. Mon conseil serait d'écrire un petit programme C à usage spécial (selon exactement ce que vous implémentez, vous pouvez l'appeler dd_done_rightou tail_headou mini-busybox).

Gilles 'SO- arrête d'être méchant'
la source
Wow, je n'ai jamais connu le yes | dd bs=1024k count=10 | wcproblème auparavant. Méchant.
Ole Tange
4

Avec dd:

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

Alternativement avec losetup:

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

Et puis dd, cat... le périphérique de boucle.

frostschutz
la source
Cela semble très centré sur Linux. J'ai également besoin du même code pour fonctionner sur AIX, FreeBSD et Solaris.
Ole Tange
0

Voici comment vous pouvez le faire:

   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

C'est tout ce qui est vraiment nécessaire - cela ne demande pas beaucoup plus. En premier lieu dd count=0 skip=1 bs=$block_size1, lseek()la saisie de fichiers régulière sur pratiquement instantanément. Il n'y a aucune chance de données manquées ou de tout autre mensonge qui soit dit à ce sujet, vous pouvez simplement rechercher directement la position de départ souhaitée. Étant donné que le descripteur de fichier appartient au shell et ddque celui-ci ne fait que l'hériter, il affectera la position de son curseur et vous pourrez donc le suivre par étapes. C'est vraiment très simple - et il n'y a pas d'outil standard mieux adapté à la tâche que dd.

Cela utilise une taille de bloc de 64k, ce qui est souvent idéal. Contrairement à la croyance populaire, les blocs plus grands ne rendent pas le ddtravail plus rapide. D'un autre côté, les petits tampons ne sont pas bons non plus. dddoit synchroniser son temps dans les appels système afin qu'il n'ait pas besoin d'attendre la copie des données en mémoire et à nouveau, mais aussi pour qu'il n'ait pas besoin d'attendre les appels système. Vous voulez donc que cela prenne suffisamment de temps pour que le prochain read()n'ait pas à attendre le dernier, mais pas tellement que vous tamponniez dans des tailles plus grandes que nécessaire.

Le premier ddsaute donc à la position de départ. Cela ne prend aucun temps. Vous pouvez appeler n'importe quel autre programme que vous aimiez à ce moment-là pour lire son stdin et il commencerait à lire directement à votre décalage d'octet souhaité. J'appelle un autre ddpour lire les ((interval / blocksize) -1)blocs de comptage sur stdout.

La dernière chose qui est nécessaire est de copier le module (le cas échéant) de l'opération de division précédente. Et c'est ça.

Soit dit en passant, ne le croyez pas quand les gens disent des faits sur leur visage sans preuve. Oui, il est possible ddde faire une courte lecture (bien que de telles choses ne soient pas possibles lors de la lecture à partir d'un périphérique de bloc sain - d'où le nom) . De telles choses ne sont possibles que si vous ne mettez pas correctement en mémoire tampon un ddflux lu à partir d'un périphérique autre qu'un bloc. Par exemple:

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

Dans les deux cas, ddcopie toutes les données. Dans le premier cas, il est possible (bien que peu probable avec cat) que certains des blocs de sortie qui copient soient des ddoctets "$ num" car ddils ne sont spécifiés que pour tamponner quoi que ce soit lorsque le tampon est spécifiquement demandé sur sa commande - ligne. bs=représente une taille de bloc maximale car le but de ce module ddest les E / S en temps réel.

Dans le deuxième exemple, je spécifie explicitement la taille de bloc de sortie et les ddlectures de tampons jusqu'à ce que des écritures complètes puissent être effectuées. Cela n'affecte pas count=ce qui est basé sur les blocs d'entrée, mais pour cela, vous avez juste besoin d'un autre dd. Toute information erronée qui vous est donnée autrement doit être ignorée.

mikeserv
la source