En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

martes, 14 de marzo de 2017

The FileCopy
cómo escribir una función de File Copy en C - pt.1

Ok, este post no tiene nada que ver con The Thing, la inmortal obra maestra de John Carpenter (aparte de la pequeña similitud del nombre). De hecho, es sólo una excusa para celebrarlo, ya que lo he visto (por milésima vez) recientemente. Sin embargo, ya que estamos, hablaremos también un poco de C...
inmortal obra maestra
Con este post veremos cómo escribir una función para realizar una copia de un file, ya que los sistemas POSIX (como Linux) no proporcionan una función específica de librería para hacerlo. Hay, por supuesto, mil formas de escribirla, y esta vez he pensado en dos versiones. ¡Vamos con la primera!
#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>

// prototipos locales
static int cpFile(const char* src, const char* dest);

// función main()
int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 3) {
        // error args
        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;
    }

    // ejecuta copia
    if (cpFile(argv[2], argv[1]) == -1) {
        // enseña error y sale
        fprintf(stderr, "%s: error: %s\n", argv[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    // sale
    return EXIT_SUCCESS;
}

// función cpFile()
static int cpFile(
    const char *dest,               // file destinación
    const char *src)                // file fuente
{
    // abre el file fuente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // abre el file destinación
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // cierra el file y return con error
        close(fd_in);
        return -1;
    }

    // r/w loop para 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) {
            // cierra el file y return con error
            close(fd_in);
            close(fd_out);
            return -1;
        }
    }

    // cierra los file
    close(fd_in);
    close(fd_out);

    // sale con el ultimo resultado de read() (0 o -1)
    return n_read;
}
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales. El main(), en este caso, sólo sirve para probar la función de copia y el programa generado se comporta (a nivel básico) como la función POSIX cp(1), que es precisamente lo que queremos emular usando la nuestra nueva función de librería.

La función que hace el trabajo la he llamada cpFile() y es bastante simple, como se ve. Utiliza el I/O sin búfer (así, por ejemplo, read(2) en lugar de fread(3)) y, aunque es muy compacta y eficiente, también trata de forma integral los errores y està escrita para ser una función de librería, por lo que no escribe nada en stderr y stdout sino simplemente realiza el trabajo y devulve un código de retorno (0 ó -1), que puede ser tratado por el llamante (en este caso el main()) para mostrar eventuales errores utilizando strerror(3) y errno. Todo el trabajo se realiza en un loop que lee un buffer en el file fuente y lo escribe en el file de destino, hasta el final del file. El resto del código es la abertura/cierre de los file y manejo de los errores. Teniendo en cuenta que utilizamos el unbuffered I/O he dimensionado el búfer de lectura/escritura utilizando la define BUFSIZ del sistema que deberia garantizar un tamaño óptimo para las operaciones de I/O

He dicho que iba a proponer dos versiones: sin modificar la función main() (que está bien para ambos casos) la versión alternativa es la siguiente:
// función cpFile()
static int cpFile(
    const char* dest,               // file destinación
    const char* src)                // file fuente
{
    // abre el file fuente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // abre el file destinación
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // cierra el file y return con error
        close(fd_in);
        return -1;
    }

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

    // cierra los file
    close(fd_in);
    close(fd_out);

    // sale con el resultado de sendfile()
    return result;
}
Como se puede ver, casi se puede sobreponer a la anterior, pero es diferente, precisamente, en la parte que realiza el trabajo de copia: en lugar del loop se utiliza la función sendfile(2), lo que nos permite realizar una copia directa y super-eficiente a nivel de kernel-space (mientras que la primera versión trabajaba en user-space).

Sin entrar en los detalles profundos que todo esto conlleva (kernel-space y user-space de los sistemas de la familia UNIX), me limitaré a señalar que esta segunda versión es mejor que la primera, pero es menos portátil, ya que la sendfile(2) tiene un comportamiento diferente en función del sistema (por ejemplo, sobre Linux se puede utilizar sólo con dal Kernel 2.6.33 hacia adelante, mientras que en macOS, por el contrario, sólo funciona hasta la versión 10.8). Y ya que estamos especificamos mejor: también la primera versión no es totalmente portátil, ya que en algunos sistemas (como el que se inicia con W y que prefiero no nombrar ni siquiera) la system call read(2) no está disponible.

Ok, entonces ya se puede adivinar que el tema del próximo post será una versión con buffered I/O de la función cpFile(), o sea una versión intrínsecamente portátil, ya que utilizará el I/O standard del C (lo que está contenido en stdio.h, para entendernos).

Y, por favor,  no contengáis la respiración esperando...

¡Hasta el próximo post!