<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Linux Sin Humo (Publicaciones sobre backup)</title><link>https://sergiobelkin.com/</link><description></description><atom:link href="https://sergiobelkin.com/categories/backup.xml" rel="self" type="application/rss+xml"></atom:link><language>es</language><copyright>Contents © 2026 &lt;a href="mailto:sebelk@gmail.com"&gt;sebelk&lt;/a&gt; 
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;
&lt;img alt="Creative Commons License BY-NC-SA"
style="border-width:0; margin-bottom:12px;"
src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"&gt;&lt;/a&gt;
</copyright><lastBuildDate>Sat, 11 Jul 2026 08:33:12 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Archivos sparse en Linux</title><link>https://sergiobelkin.com/posts/sparse-files-linux/</link><dc:creator>sebelk</dc:creator><description>&lt;figure&gt;&lt;img src="https://sergiobelkin.com/images/sparse-files-mapa.png"&gt;&lt;/figure&gt; &lt;h3 id="sparse-files-en-la-practica"&gt;Sparse files en la práctica&lt;/h3&gt;
&lt;p&gt;Un sparse file se entiende creándolo y mirándolo, no leyendo sobre él. Vamos a tratar un archivo como el disco de una máquina virtual: se crea a tamaño completo y el guest le escribe encima. En la terminal, esas escrituras las simulamos con &lt;code&gt;dd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;El "disco" de 1 GiB, todavía en blanco:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;truncate -s 1G disk.img
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Lo medimos de dos maneras —lo que declara y lo que ocupa de verdad—:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;du -h --apparent-size disk.img    # 1,0G  ← lo que declara
du -h disk.img                    # 0     ← lo que ocupa
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Declara un gigabyte y ocupa cero. Escribimos 16 MiB al principio, como el guest grabando su arranque:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;urandom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;disk&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;seek&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;notrunc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;du -h --apparent-size disk.img    # 1,0G
du -h disk.img                    # 16M
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;El tamaño declarado no se movió; el real subió los 16 MiB exactos que escribimos. Ahora otros 16 MiB, pero lejos, en el offset 512 MiB:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;urandom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;disk&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;seek&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;notrunc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;du -h disk.img                    # 32M
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Real 32 MiB, aparente todavía 1 GiB. Esos 32 MiB reales son solo las dos zonas de 16 que escribimos; entre ellas quedó un &lt;strong&gt;agujero&lt;/strong&gt; de casi 500 MiB —espacio que el archivo declara pero para el que nunca se pidió un bloque en disco—.&lt;/p&gt;
&lt;p&gt;Ahora al derecho y al revés. Llenamos parte del agujero, 128 MiB en el offset 200 MiB:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;urandom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;disk&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;seek&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;notrunc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;du -h disk.img                    # 160M
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;El real saltó a 160 MiB —los 32 de antes más los 128 nuevos—: escribir en un agujero cuesta disco. Y lo perforamos de vuelta, con &lt;code&gt;fallocate&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;fallocate --punch-hole --offset 200M --length 128M disk.img
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;du -h disk.img                    # 32M
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;El real volvió a 32 MiB: perforar libera los bloques y los devuelve al disco. Ese tramo, ahora vacío, se lee como ceros —no como basura, ni con error—:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;disk&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;skip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\0'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;En todo el recorrido el tamaño aparente no se movió de 1 GiB; el real subió al escribir y bajó al perforar. Son las dos medidas de un sparse file: lo que declara y lo que realmente ocupa en disco.&lt;/p&gt;
&lt;p&gt;Las dos direcciones del recorrido, que es lo que conviene no confundir:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operación&lt;/th&gt;
&lt;th&gt;Partís de&lt;/th&gt;
&lt;th&gt;Terminás en&lt;/th&gt;
&lt;th&gt;&lt;code&gt;st_blocks&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;escribir (&lt;code&gt;dd … seek=&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;agujero&lt;/td&gt;
&lt;td&gt;datos en disco&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sube&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;perforar (&lt;code&gt;--punch-hole&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;datos en disco&lt;/td&gt;
&lt;td&gt;agujero (se lee como ceros)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;baja&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Perforar no &lt;em&gt;escribe&lt;/em&gt; ceros: &lt;em&gt;libera&lt;/em&gt; los bloques. Que el agujero se lea después como ceros es el efecto observable; el mecanismo es la desasignación, y por eso el espacio real baja en vez de subir.&lt;/p&gt;
&lt;h3 id="dos-tamanos-no-uno"&gt;Dos tamaños, no uno&lt;/h3&gt;
&lt;p&gt;El lab mostró dos números que se mueven por separado. Tienen nombre, y el inodo guarda los dos; &lt;code&gt;stat&lt;/code&gt; los muestra juntos:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;stat -c 'aparente=%s bytes | reales=%b bloques de %B bytes' disk.img
&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;st_size&lt;/code&gt;&lt;/strong&gt; (el "aparente") es el offset del último byte del archivo más uno. Es lo que responde &lt;code&gt;ls -l&lt;/code&gt;. Es una declaración sobre el espacio de direcciones lógico del archivo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;st_blocks&lt;/code&gt;&lt;/strong&gt; (el "real") es el espacio que el archivo ocupa de verdad, contado —en Linux— en unidades fijas de 512 bytes. Es lo que informa &lt;code&gt;du&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/sparse-files-diagrama.svg"&gt;&lt;img src="https://sergiobelkin.com/images/sparse-files-diagrama.svg" alt="El archivo sparse del lab como una tira de 1 GiB: dos bloques de datos de 16 MiB en terracota, en los offsets 0 y 512 MiB, y el resto agujeros punteados. El tamaño aparente (st_size) es 1 GiB; el real (st_blocks), 32 MiB." style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;El mismo archivo, sus dos medidas: &lt;code&gt;st_size&lt;/code&gt; declara 1 GiB —el ancho lógico completo—; &lt;code&gt;st_blocks&lt;/code&gt; cuenta solo los dos bloques tocados. Entre los datos, agujeros: espacio declarado que no ocupa disco.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Un archivo denso cualquiera muestra las dos magnitudes juntas. &lt;code&gt;/usr/bin/chmod&lt;/code&gt; mide 65920 bytes (&lt;code&gt;st_size&lt;/code&gt;), pero en disco ocupa 69632: el filesystem asigna bloques enteros de 4096, y 65920 bytes necesitan 17 de esos bloques (17 × 4096 = 69632), con 3712 bytes del último reservados pero sin usar.&lt;sup id="fnref:unidades"&gt;&lt;a class="footnote-ref" href="https://sergiobelkin.com/posts/sparse-files-linux/#fn:unidades"&gt;1&lt;/a&gt;&lt;/sup&gt; En un archivo denso lo asignado iguala o supera al tamaño.&lt;/p&gt;
&lt;p&gt;Un sparse file es el caso opuesto: &lt;code&gt;st_size&lt;/code&gt; mayor que el espacio asignado, y la diferencia son los &lt;strong&gt;agujeros&lt;/strong&gt; que vimos abrirse y cerrarse en el lab.&lt;/p&gt;
&lt;h3 id="de-donde-salen-en-la-practica"&gt;De dónde salen en la práctica&lt;/h3&gt;
&lt;p&gt;Los agujeros no son una curiosidad de laboratorio. Aparecen solos, sin que nadie los pida, cada vez que un programa escribe lejos del principio del archivo o libera rangos intermedios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Imágenes de disco de máquinas virtuales.&lt;/strong&gt; Un disco &lt;code&gt;raw&lt;/code&gt; de 100 GiB con 8 GiB usados es un sparse file. &lt;code&gt;qcow2&lt;/code&gt; hace lo suyo por encima, pero el archivo subyacente también lo es.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bases de datos&lt;/strong&gt; que preasignan un archivo grande y lo llenan de a poco.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clientes de BitTorrent&lt;/strong&gt;, que crean el archivo con su tamaño final y van completando piezas fuera de orden.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core dumps&lt;/strong&gt;, que no vuelcan las regiones de memoria sin mapear.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/var/log/lastlog&lt;/code&gt;&lt;/strong&gt;, que usa el UID del usuario como posición dentro del archivo — el caso más didáctico de todos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Los filesystems que un sysadmin corporativo tiene enfrente —ext4, XFS, btrfs— soportan agujeros. También &lt;code&gt;tmpfs&lt;/code&gt;. FAT y exFAT no, y son justo los que aparecen en un pendrive o en la partición EFI: copiar un sparse file a uno de ellos lo escribe entero, porque el destino no tiene forma de representar los agujeros.&lt;/p&gt;
&lt;h3 id="ver-donde-estan-los-agujeros"&gt;Ver dónde están los agujeros&lt;/h3&gt;
&lt;p&gt;Para ver &lt;em&gt;dónde&lt;/em&gt; están los agujeros —qué rangos tienen datos y cuáles son hueco—, &lt;code&gt;xfs_io&lt;/code&gt; (del paquete &lt;code&gt;xfsprogs&lt;/code&gt;) los marca con &lt;code&gt;hole&lt;/code&gt;. Se apoya en &lt;code&gt;FIEMAP&lt;/code&gt;, un ioctl genérico del kernel que ext4, XFS y btrfs implementan por igual, así que, a pesar de su nombre, sirve en los tres.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;xfs_io -c fiemap disk.img
  0: [0..32767]: …
  1: [32768..1048575]: hole
  2: [1048576..1081343]: …
  3: [1081344..2097151]: hole
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La tercera columna —los offsets físicos en disco— la recorté con &lt;code&gt;…&lt;/code&gt;: cambia según el filesystem y no aporta acá. Los rangos van en sectores de 512 bytes: los primeros 16 MiB (&lt;code&gt;[0..32767]&lt;/code&gt;) tienen datos, el tramo &lt;code&gt;[32768..1048575]&lt;/code&gt; es un &lt;code&gt;hole&lt;/code&gt;, en el offset 512 MiB (&lt;code&gt;[1048576..1081343]&lt;/code&gt;) están los otros 16 MiB, y el resto hasta el final es otro &lt;code&gt;hole&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Hay un camino más de bajo nivel: los flags &lt;code&gt;SEEK_HOLE&lt;/code&gt; y &lt;code&gt;SEEK_DATA&lt;/code&gt; de &lt;code&gt;lseek&lt;/code&gt; (desde Linux 3.1) saltan al próximo agujero o al próximo rango con datos sin leer el archivo entero. Es el mecanismo que usan por dentro las herramientas que preservan agujeros al copiar, y el que las separa de las que re-inflan.&lt;/p&gt;
&lt;p&gt;Una advertencia sobre &lt;code&gt;filefrag&lt;/code&gt;, que suele ser lo primero que uno prueba: su conteo (&lt;code&gt;N extents found&lt;/code&gt;) mide fragmentación física, no agujeros, y se lee distinto según el filesystem —el mismo archivo con el mismo agujero da "1 extent" en XFS y "2" en btrfs—. Para ver agujeros, &lt;code&gt;fiemap&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="convertir-ceros-en-agujeros"&gt;Convertir ceros en agujeros&lt;/h4&gt;
&lt;p&gt;En el lab abrimos agujeros con &lt;code&gt;--punch-hole&lt;/code&gt;. La otra variante, &lt;code&gt;--dig-holes&lt;/code&gt;, recupera espacio: toma un archivo que ya ocupa disco lleno de ceros —una copia que se infló, una imagen descargada sin sparse— y reemplaza esas secuencias de ceros por agujeros. Un archivo de 64 MiB escritos en cero ocupa 64 MiB reales; después, cero:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;zero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;lleno&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;none&lt;/span&gt;
&lt;span class="nv"&gt;du&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lleno&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;
&lt;span class="nv"&gt;fallocate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nv"&gt;dig&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;holes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lleno&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;
&lt;span class="nv"&gt;du&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lleno&lt;/span&gt;.&lt;span class="nv"&gt;img&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;el&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;aparente&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sigue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;en&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Con &lt;code&gt;--dig-holes&lt;/code&gt; se cierra el cuadro de las tres operaciones. &lt;code&gt;--punch-hole&lt;/code&gt; y &lt;code&gt;--dig-holes&lt;/code&gt; terminan igual —un agujero, &lt;code&gt;st_blocks&lt;/code&gt; baja—, pero parten de cosas distintas:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operación&lt;/th&gt;
&lt;th&gt;Partís de&lt;/th&gt;
&lt;th&gt;Terminás en&lt;/th&gt;
&lt;th&gt;&lt;code&gt;st_blocks&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;escribir ceros (&lt;code&gt;dd if=/dev/zero&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;agujero&lt;/td&gt;
&lt;td&gt;ceros reales en disco&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sube&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;perforar (&lt;code&gt;--punch-hole&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;datos en disco&lt;/td&gt;
&lt;td&gt;agujero&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;baja&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cavar (&lt;code&gt;--dig-holes&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;ceros reales en disco&lt;/td&gt;
&lt;td&gt;agujero&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;baja&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Escribir ceros y cavarlos son inversas: la primera los materializa en disco, la segunda los recupera. Perforar es el atajo cuando ni siquiera te importa qué había en el rango.&lt;/p&gt;
&lt;h3 id="cuando-se-convierte-en-un-problema"&gt;Cuándo se convierte en un problema&lt;/h3&gt;
&lt;p&gt;El diseño se sostiene mientras el archivo se quede quieto. El problema aparece cuando algo lo &lt;strong&gt;copia&lt;/strong&gt;, porque una herramienta que lee a través de la API normal de bytes ve ceros, no agujeros. Si al escribir no vuelve a perforarlos, los materializa.&lt;/p&gt;
&lt;p&gt;Con el &lt;code&gt;disk.img&lt;/code&gt; de arriba —1 GiB aparente, 32 MiB reales—, copiado en RHEL 8 (XFS, coreutils 8.30):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operación&lt;/th&gt;
&lt;th&gt;Ocupación en destino&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cp disk.img destino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cp --sparse=never disk.img destino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat disk.img &amp;gt; destino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tar cf plain.tar disk.img&lt;/code&gt; y extraer&lt;/td&gt;
&lt;td&gt;1 GiB (y el &lt;code&gt;.tar&lt;/code&gt; pesa 1,1 GiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tar cSf sparse.tar disk.img&lt;/code&gt; y extraer&lt;/td&gt;
&lt;td&gt;64 MiB (y el &lt;code&gt;.tar&lt;/code&gt; pesa 33 MiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rsync -a disk.img destino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rsync -aS disk.img destino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Las cuatro que re-inflan —&lt;code&gt;cp --sparse=never&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;tar&lt;/code&gt; sin &lt;code&gt;-S&lt;/code&gt;, &lt;code&gt;rsync -a&lt;/code&gt;— llevan la copia a 1 GiB: materializan cada cero.&lt;sup id="fnref:fs"&gt;&lt;a class="footnote-ref" href="https://sergiobelkin.com/posts/sparse-files-linux/#fn:fs"&gt;2&lt;/a&gt;&lt;/sup&gt; Las tres que preservan el sparse —&lt;code&gt;cp&lt;/code&gt; a secas, &lt;code&gt;tar -S&lt;/code&gt;, &lt;code&gt;rsync -aS&lt;/code&gt;— dan 64 MiB. Que &lt;code&gt;cp&lt;/code&gt; sin flags preserve ya es contraintuitivo: su default es &lt;code&gt;--sparse=auto&lt;/code&gt;, detecta los agujeros del origen y los recrea; solo los pierde con &lt;code&gt;--sparse=never&lt;/code&gt; o si el destino no los soporta.&lt;/p&gt;
&lt;p&gt;Pero 64 MiB es el &lt;strong&gt;doble&lt;/strong&gt; de los 32 del origen, y no es re-inflado: los agujeros están intactos (&lt;code&gt;xfs_io -c fiemap destino&lt;/code&gt; los muestra igual), y ese doble es XFS reservando bloques de más al escribir (&lt;em&gt;speculative preallocation&lt;/em&gt;). Es una reserva transitoria —XFS la reclama con el tiempo (el barredor de &lt;code&gt;eofblocks&lt;/code&gt;) o bajo presión de espacio, y su tamaño depende de la opción de montaje &lt;code&gt;allocsize&lt;/code&gt; y de la versión—, así que el número exacto puede no reproducirse igual en otro XFS. En ext4 o btrfs la misma copia da los 32 MiB exactos: es un rasgo de XFS, no de las herramientas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tar&lt;/code&gt; sin &lt;code&gt;-S&lt;/code&gt; infla también el archivo &lt;code&gt;.tar&lt;/code&gt;.&lt;/strong&gt; Produce uno de 1,1 GiB en lugar de los 33 MiB que ocupa con &lt;code&gt;-S&lt;/code&gt;. Ese gigabyte de ceros viaja por la red, se escribe en la cinta o en el bucket, y recién se nota cuando el job de backup llena el volumen de destino.&lt;/p&gt;
&lt;p&gt;Con &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;bzip2&lt;/code&gt; o &lt;code&gt;xz&lt;/code&gt; la falla es más difícil de ver, porque un gigabyte de ceros comprime a casi nada. El &lt;code&gt;.gz&lt;/code&gt; es minúsculo y todo parece correcto. El &lt;code&gt;gunzip&lt;/code&gt; en el destino escribe el gigabyte completo, sin agujeros.&lt;/p&gt;
&lt;p&gt;De las herramientas de transferencia, &lt;code&gt;scp&lt;/code&gt; y &lt;code&gt;sftp&lt;/code&gt; no tienen ninguna noción de agujeros. Tampoco &lt;code&gt;git&lt;/code&gt;: el blob incluye todos los ceros. &lt;code&gt;dd&lt;/code&gt; los re-infla salvo que le pases &lt;code&gt;conv=sparse&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="la-regla-que-se-deduce-de-la-tabla"&gt;La regla que se deduce de la tabla&lt;/h3&gt;
&lt;p&gt;Si una herramienta ofrece un flag explícito de sparse —&lt;code&gt;rsync -S&lt;/code&gt;, &lt;code&gt;tar -S&lt;/code&gt;, &lt;code&gt;dd conv=sparse&lt;/code&gt;, &lt;code&gt;cp --sparse=always&lt;/code&gt;, &lt;code&gt;restic restore --sparse&lt;/code&gt;, &lt;code&gt;borg extract --sparse&lt;/code&gt;— es señal de que &lt;strong&gt;por defecto no lo hace&lt;/strong&gt;, o de que su default es una heurística que conviene verificar. Las que ni siquiera ofrecen la opción re-inflan siempre.&lt;/p&gt;
&lt;p&gt;En backup empresarial el patrón se repite: Bacula y Bareos manejan agujeros solo si activás la directiva &lt;code&gt;Sparse = yes&lt;/code&gt;, que viene desactivada. &lt;code&gt;restic&lt;/code&gt; y &lt;code&gt;borg&lt;/code&gt; deduplican los ceros muy bien —el repositorio queda chico— pero la restauración los re-infla si no le pedís lo contrario.&lt;/p&gt;
&lt;p&gt;Hay una excepción importante, para no exagerar el problema: el backup &lt;strong&gt;a nivel de imagen o de bloque&lt;/strong&gt; no lee archivos, lee bloques asignados del dispositivo. Los agujeros no le representan un problema. El riesgo descrito acá aplica al backup y la restauración &lt;strong&gt;a nivel de archivo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;El origen nunca se queja: el espacio real de un sparse file es minúsculo. La medición que importa no es la del archivo que tenés, sino la del que vas a producir —el destino de la copia, el &lt;code&gt;.tar&lt;/code&gt; que se sube, el volumen donde se restaura—. Un &lt;code&gt;du -h&lt;/code&gt; contra un &lt;code&gt;du -h --apparent-size&lt;/code&gt; en el origen te dice cuánto puede crecer; los flags de la herramienta te dicen si va a crecer.&lt;/p&gt;
&lt;h3 id="fuentes-y-mas-recursos"&gt;Fuentes y más recursos&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/stat.3type.html"&gt;&lt;code&gt;stat(3type)&lt;/code&gt;&lt;/a&gt; — define los campos de &lt;code&gt;struct stat&lt;/code&gt;. Sobre &lt;code&gt;st_blocks&lt;/code&gt;: "the number of blocks allocated to the file, in 512-byte units". El &lt;code&gt;stat(2)&lt;/code&gt; advierte que esa unidad no es universal (HP-UX cuenta en 1024, AIX en 4K); en Linux es 512.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/lseek.2.html"&gt;&lt;code&gt;lseek(2)&lt;/code&gt;&lt;/a&gt; — define &lt;code&gt;SEEK_HOLE&lt;/code&gt; y &lt;code&gt;SEEK_DATA&lt;/code&gt;, el mecanismo que permite recorrer agujeros sin leer el archivo entero. Disponibles desde Linux 3.1.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/fallocate.1.html"&gt;&lt;code&gt;fallocate(1)&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;--punch-hole&lt;/code&gt; para perforar un rango y &lt;code&gt;--dig-holes&lt;/code&gt; para convertir en agujeros las secuencias de ceros ya escritas.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/filefrag.8.html"&gt;&lt;code&gt;filefrag(8)&lt;/code&gt;&lt;/a&gt; — inspecciona el mapa de extents de un archivo; viene en &lt;code&gt;e2fsprogs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 1 cp&lt;/code&gt; — la sección de &lt;code&gt;--sparse&lt;/code&gt; aclara que la detección por defecto (&lt;code&gt;auto&lt;/code&gt;) usa "un mecanismo heurístico muy básico": por eso conviene medir en lugar de asumir.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 1 tar&lt;/code&gt; — &lt;code&gt;-S&lt;/code&gt;/&lt;code&gt;--sparse&lt;/code&gt; y &lt;code&gt;--hole-detection=seek|raw&lt;/code&gt;, que usa &lt;code&gt;SEEK_HOLE&lt;/code&gt; cuando puede y cae a leer el archivo crudo cuando no.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restic.readthedocs.io/en/stable/050_restore.html"&gt;Restauración con restic&lt;/a&gt; — documenta que "By default, restic does not restore files as sparse"; hay que pasar &lt;code&gt;--sparse&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://borgbackup.readthedocs.io/en/stable/usage/extract.html"&gt;&lt;code&gt;borg extract&lt;/code&gt;&lt;/a&gt; — su opción &lt;code&gt;--sparse&lt;/code&gt; recrea los agujeros a partir de los bloques de ceros.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:unidades"&gt;
&lt;p&gt;Ese mismo espacio asignado lo expresa cada herramienta en su propia unidad, y los números no siempre coinciden: &lt;code&gt;stat&lt;/code&gt; da &lt;code&gt;st_blocks = 136&lt;/code&gt; (136 × 512 = 69632), &lt;code&gt;ls -ls&lt;/code&gt; pone &lt;code&gt;68&lt;/code&gt; en su primera columna (68 × 1024 = 69632) y &lt;code&gt;du&lt;/code&gt; dice &lt;code&gt;68K&lt;/code&gt;. La diferencia entre 136 y 68 es solo la unidad —512 bytes contra 1024, el default de GNU &lt;code&gt;ls&lt;/code&gt;—. Con &lt;code&gt;POSIXLY_CORRECT&lt;/code&gt;, o con &lt;code&gt;ls -s --block-size=512&lt;/code&gt;, &lt;code&gt;ls -s&lt;/code&gt; baja a 512 y coincide con &lt;code&gt;st_blocks&lt;/code&gt;. &lt;a class="footnote-backref" href="https://sergiobelkin.com/posts/sparse-files-linux/#fnref:unidades" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:fs"&gt;
&lt;p&gt;En btrfs con compresión (el default de Fedora) esas cifras de 1 GiB salen mucho más chicas: los ceros reescritos se comprimen a casi nada. El archivo se re-infla igual en el espacio lógico, pero el filesystem disimula el costo en disco. &lt;a class="footnote-backref" href="https://sergiobelkin.com/posts/sparse-files-linux/#fnref:fs" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>backup</category><category>filesystems</category><category>sparse-files</category><category>sysadmin</category><guid>https://sergiobelkin.com/posts/sparse-files-linux/</guid><pubDate>Fri, 10 Jul 2026 10:00:00 GMT</pubDate></item></channel></rss>