Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 11 marzo 2017

The FileCopy
come scrivere una funzione di File Copy in C - pt.1

Ok, questo post non centra niente con The Thing, il capolavoro immortale di John Carpenter (a parte la piccola assonanza del nome). Anzi, è solo una scusa per celebrarlo, dato che l'ho rivisto (per la millesima volta) da poco. Comunque, dato che ci siamo, parleremo anche un po' di C...

capolavoro immortale
Con questo post vedremo come scrivere una funzione per fare una copia di un file, dato che i sistemi POSIX (come Linux) non prevedono una funzione specifica di libreria per farlo. Ci sono, evidentemente, mille maniere per scriverla, e questa volta ne ho pensate due. Vai con la prima!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sendfile.h>

// prototipi locali
static int cpFile(const char* src, const char* dest);

// funzione main()
int main(int argc, char *argv[])
{
    // test argumenti
    if (argc != 3) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s srcfile destfile [e.g.: %s try.c try.save]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // esegue copy
    if (cpFile(argv[2], argv[1]) == -1) {
        // mostra errore ed esce
        fprintf(stderr, "%s: error: %s\n", argv[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    // esce
    return EXIT_SUCCESS;
}

// funzione cpFile()
static int cpFile(
    const char *dest,               // file destinazione
    const char *src)                // file sorgente
{
    // apre il file sorgente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // apre il file destinazione
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // chiude il file e return con errore
        close(fd_in);
        return -1;
    }

    // r/w loop per la copia usando unbuffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = read(fd_in, buffer, sizeof(buffer))) > 0) {
        // write buffer
        if (write(fd_out, buffer, n_read) == -1) {
            // chiude i file e return con errore
            close(fd_in);
            close(fd_out);
            return -1;
        }
    }

    // chiude i file
    close(fd_in);
    close(fd_out);

    // esce con l'ultimo risultato di read() (0 o -1)
    return n_read;
}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale. Il main(), in questo caso, serve solo per testare la funzione di copia e il programma generato si comporta (a livello basico) come la funzione POSIX cp(1), che è proprio quella che vogliamo emulare usando la nostra nuova funzione di libreria.

La funzione che esegue il lavoro l'ho chiamata cpFile() ed è abbastanza semplice, come si vede. Usa l'I/O non bufferizzato (quindi, per esempio, read(2) invece di fread(3)) e, pur essendo compattissima ed efficiente, tratta anche in maniera esaustiva gli errori ed è scritta per essere una funzione di libreria, quindi non scrive nulla su stderr e stdout ma si limita a eseguire il lavoro e a restituire un codice di ritorno (0 o -1) che può essere trattato dal chiamante (in questo caso il main()) per visualizzare eventuali errori usando strerror(3) ed errno. Tutto il lavoro viene eseguito in un loop che legge un buffer dal file sorgente e lo scrive nel file destinazione, fino alla fine del file. Il resto del codice è apertura/chiusura dei file e trattamento degli errori. Visto che usiamo l'unbuffered I/O ho dimensionato il buffer di lettura/scrittura usando la define BUFSIZ del sistema che dovrebbe garantire la dimensione ottimale per le operazioni di I/O.

Avevo detto che avrei proposto due versioni: fermo restando il main() (che va bene per entrambi i casi) la versione alternativa è questa:
// funzione cpFile()
static int cpFile(
    const char* dest,               // file destinazione
    const char* src)                // file sorgente
{
    // apre il file sorgente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // apre il file destinazione
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // chiude il file e return con errore
        close(fd_in);
        return -1;
    }

    // copia in kernel-space usando la funzione sendfile()
    off_t bytesCopied = 0;
    struct stat fileinfo = {0};
    fstat(fd_in, &fileinfo);
    int result = sendfile(fd_out, fd_in, &bytesCopied, fileinfo.st_size);

    // chiude i file
    close(fd_in);
    close(fd_out);

    // esce con il risultato di sendfile()
    return result;
}
Come vedete è quasi sovrapponibile alla precedente ma è diversa proprio nella parte che esegue il lavoro di copia: al posto del loop viene usata la funzione sendfile(2), che ci permette di eseguire una copia diretta e super-efficiente a livello kernel-space (mentre la prima versione lavorava in user-space).

Senza entrare nei dettagli profondi che tutto questo comporta (kernel-space e user-space dei sistemi della famiglia UNIX), mi limito a precisare che questa seconda versione è migliore della prima ma è meno portabile, visto che la sendfile(2) ha comportamenti diversi in base al sistema (per esempio su Linux si può usare solo dal Kernel 2.6.33 in avanti, mentre su macOS, viceversa, funziona solo fino alla versione 10.8). E già che ci siamo specifichiamo meglio: anche la prima versione non è completamente portabile, visto che su alcuni sistemi (tipo quello che comincia con W e che preferisco non nominare neanche) la system call read(2) non c'è.

Ok, allora potete già intuire che l'argomento del prossimo post sarà una versione con buffered I/O della funzione cpFile(), ossia una versione intrinsecamente portabile, già che userà l'I/O standard del C (quello contenuto in stdio.h, per intenderci).

Non trattenete il respiro nell'attesa, mi raccomando...

Ciao e al prossimo post!