Copia un file in modo sano, sicuro ed efficiente


305

Cerco un buon modo per copiare un file (binario o di testo). Ho scritto diversi esempi, tutti lavorano. Ma voglio sentire l'opinione dei programmatori esperti.

Mi mancano buoni esempi e cerco un modo che funzioni con C ++.

ANSI-C-WAY

#include <iostream>
#include <cstdio>    // fopen, fclose, fread, fwrite, BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE default is 8192 bytes
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    FILE* source = fopen("from.ogv", "rb");
    FILE* dest = fopen("to.ogv", "wb");

    // clean and more secure
    // feof(FILE* stream) returns non-zero if the end of file indicator for stream is set

    while (size = fread(buf, 1, BUFSIZ, source)) {
        fwrite(buf, 1, size, dest);
    }

    fclose(source);
    fclose(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

POSIX-WAY (K&R lo usa in "Il linguaggio di programmazione C", più basso livello)

#include <iostream>
#include <fcntl.h>   // open
#include <unistd.h>  // read, write, close
#include <cstdio>    // BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE defaults to 8192
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    while ((size = read(source, buf, BUFSIZ)) > 0) {
        write(dest, buf, size);
    }

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

KISS-C ++ - Streambuffer-WAY

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    dest << source.rdbuf();

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

COPIA-ALGORITMO-C ++ - WAY

#include <iostream>
#include <fstream>
#include <ctime>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    istreambuf_iterator<char> begin_source(source);
    istreambuf_iterator<char> end_source;
    ostreambuf_iterator<char> begin_dest(dest); 
    copy(begin_source, end_source, begin_dest);

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

OWN-BUFFER-C ++ - WAY

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    // file size
    source.seekg(0, ios::end);
    ifstream::pos_type size = source.tellg();
    source.seekg(0);
    // allocate memory for buffer
    char* buffer = new char[size];

    // copy file    
    source.read(buffer, size);
    dest.write(buffer, size);

    // clean up
    delete[] buffer;
    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

LINUX-WAY // richiede kernel> = 2.6.33

#include <iostream>
#include <sys/sendfile.h>  // sendfile
#include <fcntl.h>         // open
#include <unistd.h>        // close
#include <sys/stat.h>      // fstat
#include <sys/types.h>     // fstat
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    // struct required, rationale: function stat() exists also
    struct stat stat_source;
    fstat(source, &stat_source);

    sendfile(dest, source, 0, stat_source.st_size);

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

Ambiente

  • GNU / LINUX (Archlinux)
  • Kernel 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Utilizzo di RUNLEVEL 3 (multiutente, rete, terminale, nessuna GUI)
  • INTEL SSD-Postville 80 GB, riempito fino al 50%
  • Copia un file OGG-VIDEO-FILE da 270 MB

I passaggi per riprodurre

 1. $ rm from.ogg
 2. $ reboot                           # kernel and filesystem buffers are in regular
 3. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file
 4. $ sha256sum *.ogv                  # checksum
 5. $ rm to.ogg                        # remove copy, but no sync, kernel and fileystem buffers are used
 6. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file

Risultati (tempo CPU utilizzato)

Program  Description                 UNBUFFERED|BUFFERED
ANSI C   (fread/frwite)                 490,000|260,000  
POSIX    (K&R, read/write)              450,000|230,000  
FSTREAM  (KISS, Streambuffer)           500,000|270,000 
FSTREAM  (Algorithm, copy)              500,000|270,000
FSTREAM  (OWN-BUFFER)                   500,000|340,000  
SENDFILE (native LINUX, sendfile)       410,000|200,000  

La dimensione del file non cambia.
sha256sum stampa gli stessi risultati.
Il file video è ancora riproducibile.

Domande

  • Quale metodo preferiresti?
  • Conosci soluzioni migliori?
  • Vedi errori nel mio codice?
  • Conosci un motivo per evitare una soluzione?

  • FSTREAM (KISS, Streambuffer)
    Mi piace molto questo, perché è davvero breve e semplice. Per quanto ne so l'operatore << è sovraccarico per rdbuf () e non converte nulla. Corretta?

Grazie

Aggiornamento 1
Ho modificato la fonte in tutti gli esempi in quel modo, che l'apertura e la chiusura dei descrittori di file è inclusa nella misurazione di clock () . Non ci sono altri cambiamenti significativi nel codice sorgente. I risultati non sono cambiati! Ho anche usato il tempo per ricontrollare i miei risultati.


Esempio di aggiornamento 2 ANSI C modificato: la condizione del ciclo while non chiama più feof () invece ho spostato fread () nella condizione. Sembra che il codice funzioni ora 10.000 orologi più velocemente.

La misurazione è cambiata: i risultati precedenti erano sempre bufferizzati, perché ho ripetuto la vecchia riga di comando rm to.ogv && sync && time ./program per ciascun programma alcune volte. Ora riavvio il sistema per ogni programma. I risultati senza buffer sono nuovi e non mostrano alcuna sorpresa. I risultati senza buffer non sono cambiati davvero.

Se non elimino la vecchia copia, i programmi reagiscono in modo diverso. La sovrascrittura di un file esistente memorizzato nel buffer è più veloce con POSIX e SENDFILE, tutti gli altri programmi sono più lenti. Forse le opzioni troncate o create hanno un impatto su questo comportamento. Ma sovrascrivere i file esistenti con la stessa copia non è un caso d'uso reale.

L'esecuzione della copia con cp richiede 0,44 secondi senza buffer e 0,30 secondi con buffer. Quindi cp è un po 'più lento dell'esempio POSIX. Mi sta bene.

Forse aggiungo anche campioni e risultati di mmap () e copy_file()da boost :: filesystem.

Aggiornamento 3
L'ho inserito anche in una pagina del blog e l'ho esteso un po '. Incluso splice () , che è una funzione di basso livello del kernel Linux. Forse seguiranno altri esempi con Java. http://www.ttyhoney.com/blog/?page_id=69


5
fstreamsicuramente è una buona opzione per le operazioni sui file.
chris,


29
Hai dimenticato il modo pigro: system ("cp from.ogv to.ogv");
fbafelipe,

3
#include <copyfile.h> copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York,

3
Ci scusiamo per averci contattato così tardi, ma non descriverei nessuno di questi come "sicuri", poiché non hanno alcun errore nella gestione.
Richard Kettlewell,

Risposte:


260

Copia un file in modo sano:

#include <fstream>

int main()
{
    std::ifstream  src("from.ogv", std::ios::binary);
    std::ofstream  dst("to.ogv",   std::ios::binary);

    dst << src.rdbuf();
}

È così semplice e intuitivo da leggere che vale il costo aggiuntivo. Se lo facessimo molto, meglio ricorrere alle chiamate del sistema operativo al file system. Sono sicuro che boostha un metodo per copiare file nella sua classe di filesystem.

Esiste un metodo C per interagire con il file system:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);

29
copyfilenon è portatile; Penso che sia specifico per Mac OS X. Certamente non esiste su Linux. boost::filesystem::copy_fileè probabilmente il modo più portatile per copiare un file tramite il file system nativo.
Mike Seymour,

4
@MikeSeymour: copyfile () sembra essere un'estensione BSD.
Martin York,

10
@ duedl0r: No. Gli oggetti hanno distruttori. Il distruttore per gli stream chiama automaticamente close (). codereview.stackexchange.com/q/540/507
Martin York,

11
@ duedl0r: Sì. Ma è come dire "se il sole tramonta". Puoi correre molto velocemente verso ovest e potresti allungare leggermente la giornata, ma il sole tramonterà. A meno che non si disponga di bug e di perdite di memoria (andrà fuori campo). Ma dal momento che non esiste una gestione dinamica della memoria qui non può esserci una perdita e andranno fuori campo (proprio come il sole tramonterà).
Martin York,

6
Quindi semplicemente
avvolgilo

62

Con C ++ 17 il modo standard per copiare un file includerà l' <filesystem>intestazione e usando:

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to);

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to,
                std::filesystem::copy_options options);

Il primo modulo è equivalente al secondo con copy_options::noneusato come opzioni (vedi anche copy_file).

La filesystemlibreria è stata originariamente sviluppata come boost.filesysteme infine unita a ISO C ++ a partire da C ++ 17.


2
Perché non esiste una singola funzione con un argomento predefinito, come bool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none);?
Jepessen,

2
@Jepessen Non ne sono sicuro. Forse non importa davvero .
manlio,

@Jepessen nella libreria standard, il codice pulito è fondamentale. Avere sovraccarichi (anziché una funzione con parametri predefiniti) rende più chiara l'intenzione del programmatore.
Marc.2377,

@Peter Questa ora dovrebbe probabilmente essere la risposta accettata dato che C ++ 17 è disponibile.
Martin York,

21

Troppi!

Il buffer "ANSI C" è ridondante, poiché a FILEè già bufferizzato. (La dimensione di questo buffer interno è ciò BUFSIZche definisce effettivamente.)

"OWN-BUFFER-C ++ - WAY" sarà lento mentre procede fstream, il che fa molto dispacciamento virtuale e mantiene di nuovo i buffer interni o ogni oggetto stream. (Il "COPY-ALGORITHM-C ++ - WAY" non ne risente, poiché la streambuf_iteratorclasse ignora il layer stream.)

Preferisco "COPY-ALGORITHM-C ++ - WAY", ma senza costruirne uno fstream, basta creare std::filebufistanze nude quando non è necessaria alcuna formattazione effettiva.

Per prestazioni non elaborate, non puoi battere i descrittori di file POSIX. È brutto ma portatile e veloce su qualsiasi piattaforma.

Il modo in cui Linux sembra essere incredibilmente veloce - forse il sistema operativo ha lasciato tornare la funzione prima che l'I / O fosse terminato? In ogni caso, non è abbastanza portatile per molte applicazioni.

EDIT : Ah, "Linux nativo" potrebbe migliorare le prestazioni intercalando letture e scritture con I / O asincrono. Lasciare accumulare comandi può aiutare il driver del disco a decidere quando è meglio cercare. Potresti provare Boost Asio o pthreads per il confronto. Per quanto riguarda "impossibile battere i descrittori di file POSIX" ... è vero se stai facendo qualcosa con i dati, non solo copiando ciecamente.


ANSI C: Ma devo dare una dimensione alla funzione fread / fwrite? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter

@PeterWeber Bene, sì, è vero che BUFSIZ ha un valore pari a qualsiasi altro e probabilmente accelererà le cose rispetto a uno o "pochi" caratteri alla volta. Ad ogni modo, la misurazione delle prestazioni conferma che non è il metodo migliore in ogni caso.
Potatoswatter,

1
Non ho una comprensione approfondita di questo, quindi dovrei stare attento con ipotesi e opinioni. Linux-Way funziona in Kernelspace afaik. Questo dovrebbe evitare il lento cambio di contesto tra Kernelspace e Userspace? Domani darò di nuovo un'occhiata alla manpage di sendfile. Qualche tempo fa Linus Torvalds ha detto che non gli piace Userspace-Filesystems per lavori pesanti. Forse sendfile è un esempio positivo per il suo punto di vista?
Peter,

5
" sendfile()copia i dati tra un descrittore di file e un altro. Poiché questa copia viene eseguita all'interno del kernel, sendfile()è più efficiente della combinazione di read(2)e write(2), che richiederebbe il trasferimento di dati da e verso lo spazio utente.": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert

1
Potresti pubblicare un esempio di utilizzo di filebufoggetti grezzi ?
Kerrek SB,

14

Voglio fare la nota molto importante che il metodo LINUX usando sendfile () ha un grosso problema in quanto non può copiare file di dimensioni superiori a 2 GB! L'ho implementato seguendo questa domanda e ho riscontrato problemi perché lo stavo usando per copiare file HDF5 di dimensioni di molti GB.

http://man7.org/linux/man-pages/man2/sendfile.2.html

sendfile () trasferirà al massimo 0x7ffff000 (2.147.479.552) byte, restituendo il numero di byte effettivamente trasferiti. (Questo è vero su entrambi i sistemi a 32 e 64 bit.)


1
sendfile64 () ha lo stesso problema?
graywolf,

1
@Paladin Sembra che sendfile64 sia stato sviluppato per aggirare questa limitazione. Dalla pagina man: "" "La chiamata di sistema originale sendfile () di Linux non è stata progettata per gestire grandi offset di file. Di conseguenza, Linux 2.4 ha aggiunto sendfile64 (), con un tipo più ampio per l'argomento offset. La funzione wrapper glibc sendfile ()
gestisce in modo

sendfile64 ha lo stesso problema a quanto pare. Tuttavia, l'uso del tipo offset off64_tconsente di utilizzare un ciclo per copiare file di grandi dimensioni, come mostrato in una risposta alla domanda collegata.
pcworld,

questo è vero in man: 'Nota che una chiamata riuscita a sendfile () può scrivere meno byte di quanto richiesto; il chiamante dovrebbe essere pronto a ritentare la chiamata in caso di byte non inviati. " sendfile o sendfile64 potrebbe richiedere di essere chiamato all'interno di un ciclo fino a quando non viene eseguita la copia completa.
Philippe Lhardy,

2

Qt ha un metodo per copiare i file:

#include <QFile>
QFile::copy("originalFile.example","copiedFile.example");

Nota che per usare questo devi installare Qt (istruzioni qui ) e includerlo nel tuo progetto (se stai usando Windows e non sei un amministratore, puoi invece scaricare Qt qui ). Vedi anche questa risposta .


1
QFile::copyè ridicolmente lento a causa del suo buffering a 4k .
Nicolas Holthaus,

1
La lentezza è stata corretta nelle versioni più recenti di Qt. Sto usando 5.9.2e la velocità è alla pari con l'implementazione nativa. Btw. dando un'occhiata al codice sorgente, Qt sembra effettivamente chiamare l'implementazione nativa.
VK,

1

Per chi ama il boost:

boost::filesystem::path mySourcePath("foo.bar");
boost::filesystem::path myTargetPath("bar.foo");

// Variant 1: Overwrite existing
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::overwrite_if_exists);

// Variant 2: Fail if exists
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::fail_if_exists);

Si noti che boost :: filesystem :: path è disponibile anche come wpath per Unicode. E che potresti anche usare

using namespace boost::filesystem

se non ti piacciono quei nomi lunghi


La libreria di filesystem di Boost è una delle eccezioni che richiede la sua compilazione. Cordiali saluti!
SimonC,

0

Non sono sicuro di quale sia un "buon modo" di copiare un file, ma supponendo che "buono" significhi "veloce", potrei ampliare un po 'l'argomento.

I sistemi operativi attuali sono stati a lungo ottimizzati per gestire l'esecuzione della copia del file del mulino. Nessun codice intelligente lo batterà. È possibile che alcune varianti delle tue tecniche di copia si dimostrino più veloci in alcuni scenari di test, ma molto probabilmente andrebbero peggio in altri casi.

In genere, la sendfilefunzione probabilmente ritorna prima che la scrittura sia stata impegnata, dando così l'impressione di essere più veloce del resto. Non ho letto il codice, ma è sicuramente perché alloca il proprio buffer dedicato, scambiando memoria per tempo. E il motivo per cui non funzionerà per file di dimensioni superiori a 2 GB.

Fintanto che hai a che fare con un numero limitato di file, tutto accade all'interno di vari buffer (il runtime C ++ è il primo se usi iostream, quelli interni del sistema operativo, apparentemente un buffer extra di dimensioni file nel caso di sendfile). L'accesso ai supporti di memorizzazione effettivi viene effettuato solo dopo che un numero sufficiente di dati è stato spostato per valere la pena di girare un disco rigido.

Suppongo che potresti migliorare leggermente le prestazioni in casi specifici. In cima alla mia testa:

  • Se stai copiando un grosso file sullo stesso disco, usare un buffer più grande del sistema operativo potrebbe migliorare un po 'le cose (ma probabilmente stiamo parlando di gigabyte qui).
  • Se vuoi copiare lo stesso file su due diverse destinazioni fisiche, probabilmente aprirai i tre file più velocemente rispetto a chiamarne due in copy_filesequenza (anche se difficilmente noterai la differenza finché il file si adatta alla cache del sistema operativo)
  • Se hai a che fare con molti piccoli file su un HDD, potresti volerli leggere in batch per ridurre al minimo i tempi di ricerca (anche se il sistema operativo memorizza già nella cache le voci della directory per evitare la ricerca di file folli e piccoli probabilmente ridurrà drasticamente la larghezza di banda del disco).

Ma tutto ciò non rientra nell'ambito di una funzione di copia file per scopi generici.

Quindi secondo il mio programmatore probabilmente discutibile, una copia del file C ++ dovrebbe semplicemente usare la file_copyfunzione dedicata C ++ 17 , a meno che non si sappia di più sul contesto in cui si verifica la copia del file e alcune strategie intelligenti possono essere escogitate per superare in astuzia il sistema operativo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.