Si sabe que usarán un Linux> 2.6.17, splice()
es la forma de hacer copia cero en Linux:
//using some default parameters for clarity below. Don't do this in production.
#define splice(a, b, c) splice(a, 0, b, 0, c, 0)
int p[2];
pipe(p);
int out = open(OUTFILE, O_WRONLY);
int in = open(INFILE, O_RDONLY)
while(splice(p[0], out, splice(in, p[1], 4096))>0);
Desafortunadamente, no puedes usar sendfile()
aquí porque el destino no es un socket. (El nombre sendfile()
viene de send()
+ "archivo").
Para copia cero, puede usar splice()
como lo sugiere @Dave. (Excepto que no será una copia cero; será "una copia" desde la memoria caché de la página del archivo de origen a la memoria caché de la página del archivo de destino).
Sin embargo... (a) splice()
es específico de Linux; y (b) es casi seguro que puede hacerlo igual de bien usando interfaces portátiles, siempre que las use correctamente.
En resumen, use open()
+ read()
+ write()
con un pequeño amortiguador temporal. Sugiero 8K. Entonces su código se vería así:
int in_fd = open("source", O_RDONLY);
assert(in_fd >= 0);
int out_fd = open("dest", O_WRONLY);
assert(out_fd >= 0);
char buf[8192];
while (1) {
ssize_t read_result = read(in_fd, &buf[0], sizeof(buf));
if (!read_result) break;
assert(read_result > 0);
ssize_t write_result = write(out_fd, &buf[0], read_result);
assert(write_result == read_result);
}
Con este bucle, copiará 8K de la memoria caché de la página in_fd en la memoria caché L1 de la CPU y, a continuación, los escribirá desde la memoria caché L1 en la memoria caché de la página out_fd. Luego, sobrescribirá esa parte del caché L1 con el siguiente fragmento de 8K del archivo, y así sucesivamente. El resultado neto es que los datos en buf
en realidad, nunca se almacenará en la memoria principal (excepto tal vez una vez al final); desde el punto de vista de la RAM del sistema, esto es tan bueno como usar "copia cero" splice()
. Además, es perfectamente portátil para cualquier sistema POSIX.
Tenga en cuenta que el pequeño búfer es clave aquí. Las CPU modernas típicas tienen aproximadamente 32 K para la memoria caché de datos L1, por lo que si hace que el búfer sea demasiado grande, este enfoque será más lento. Posiblemente mucho, mucho más lento. Así que mantenga el búfer en el rango de "pocos kilobytes".
Por supuesto, a menos que su subsistema de disco sea muy, muy rápido, el ancho de banda de la memoria probablemente no sea su factor limitante. Entonces recomendaría posix_fadvise
para que el núcleo sepa lo que está haciendo:
posix_fadvise(in_fd, 0, 0, POSIX_FADV_SEQUENTIAL);
Esto le dará una pista al kernel de Linux de que su maquinaria de lectura anticipada debería ser muy agresiva.
También sugeriría usar posix_fallocate
para preasignar el almacenamiento para el archivo de destino. Esto le dirá con anticipación si se quedará sin disco. Y para un núcleo moderno con un sistema de archivos moderno (como XFS), ayudará a reducir la fragmentación en el archivo de destino.
Lo último que recomendaría es mmap
. Por lo general, es el enfoque más lento de todos gracias a la paliza TLB. (Los núcleos muy recientes con "páginas enormes transparentes" podrían mitigar esto; no lo he intentado recientemente. Pero ciertamente solía ser muy malo. Así que solo me molestaría en probar mmap
si tiene mucho tiempo para comparar y un kernel muy reciente).
[Actualizar]
Hay algunas dudas en los comentarios sobre si splice
de un archivo a otro es copia cero. Los desarrolladores del kernel de Linux llaman a esto "robo de página". Tanto la página man para splice
y los comentarios en el código fuente del kernel dicen que el SPLICE_F_MOVE
flag debe proporcionar esta funcionalidad.
Desafortunadamente, el soporte para SPLICE_F_MOVE
fue retirado en 2.6.21 (en 2007) y nunca reemplazado. (Los comentarios en las fuentes del kernel nunca se actualizaron). Si busca en las fuentes del kernel, encontrará SPLICE_F_MOVE
en realidad no se hace referencia en ninguna parte. El último mensaje que puedo encontrar (de 2008) dice que está "esperando un reemplazo".
La conclusión es que splice
de un archivo a otro llamadas memcpy
mover los datos; es no copia cero. Esto no es mucho mejor que lo que puedes hacer en el espacio de usuario usando read
/write
con pequeños búferes, por lo que es mejor que se ciña a las interfaces portátiles estándar.
Si alguna vez se vuelve a agregar el "robo de páginas" al kernel de Linux, entonces los beneficios de splice
sería mucho mayor. (E incluso hoy en día, cuando el destino es un socket, se obtiene una verdadera copia cero, lo que hace que splice
más atractivo). Pero para el propósito de esta pregunta, splice
no te compra mucho.