0

I am trying to implement the following task for hours now. But it is still not working and I don't really know any further.

The Task:
A c program which list all sparse files in the current directory if you pass the parameter -s in the command line or the programm should print the number of disk blocks that already represent gaps in the file and the number of disk blocks that are 0-filled but take up disk space if you use a -c instead of -s.

The code:

int main(int argc, char* argv[]) {
    if(argc > 3) {
       fprintf(stderr,"usage: sparse-files [-s|-c <file name>]\n");
       exit(EXIT_FAILURE);
    }
    DIR *dirp;
    struct dirent *dp;
    struct stat st = {0};
    int option, sflag = 0, cflag = 0;

    if ((dirp = opendir(".")) == NULL)
        perror("Could not open '.'");

    while((option = getopt(argc, argv,"sc:")) != -1) {
        switch(option) {
            case 'c':
                cflag = 1;
                break;
            case 's':
                sflag = 1;
                break;
            default:
                break;
            }
    }

    off_t sz;
    while((dp = readdir(dirp)) != NULL) {
        int counter = 0, counter2 = 0;
        if(dp->d_type == DT_REG) {
            if (sflag) {
                char * file = dp->d_name;
                if(stat(file, &st) == -1)
                    perror("stat()");
                sz = (st.st_size + st.st_blksize -1) & ~st.st_blksize;
                if ((st.st_blocks * st.st_blksize) < sz)
                    printf("%s\n", dp->d_name);
            } else if (cflag) {
                char * file = dp->d_name;
                if(stat(file, &st) == -1)
                    perror("stat()");
                int fd = open(file, O_RDONLY);
                sz = (st.st_size + st.st_blksize -1) & ~st.st_blksize;
                if ((st.st_blocks * st.st_blksize) < sz) {
                    while(lseek(fd, 0, SEEK_HOLE) != -1)
                        counter++;
                    while(lseek(fd, 0, SEEK_DATA) != -1) 
                        counter2++;
                    printf("%d %d %s\n", counter, counter2, file);
                    close(fd);
                }
            }
    }

    closedir(dirp);
    return 0;
}

I really don't know how to deal with it. I really hope someone may be able to help.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118

1 Answers1

3

As you probably know, given struct stat info as returned by a call to stat(), info.st_size is the total size of the file in bytes, and info.st_blocks*512 is the number of bytes stored on disk.

In Linux, filesystems store data in aligned chunks of info.st_blksize bytes. (This also means that info.st_blocks*512 can be larger than info.st_size (by at most info.st_blksize-1 bytes). It can also be smaller, if the file is sparse.)

Both unstored data (holes in a sparse file), and explicitly zeroed stored data, read as zeroes.

If you want to know how many zero-filled blocks there are in the file, you will need to read the entire file. Use a buffer with size an integer multiple of info.st_blksize bytes. For each aligned block of st_blksize bytes, check if they are all zeroes. Let the total number of blocks (including the last possibly partial block) be total_blocks, and the number of blocks with all zero contents be zero_blocks.

    struct stat  info;

    /* Number of filesystem blocks for the file */
    total_blocks = info.st_size / info.st_blksize
                 + (info.st_size % info.st_blksize) ? 1 : 0;

    /* Number of bytes stored for the file */
    stored_bytes = 512 * info.st_blocks;

    /* Number of filesystem blocks used for file data */
    stored_blocks = stored_bytes / info.st_blksize
                  + (stored_bytes % info.st_blksize) ? 1 : 0;

    /* Number of sparse blocks */
    sparse_blocks = total_blocks - stored_blocks;

    /* TODO: count zero_blocks,
     *       by reading file in info.st_blksize chunks,
     *       and saving the number of all-zero chunks
     *       in zero_blocks. */

    /* Number of stored zero blocks */
    zeroed_blocks = zero_blocks - sparse_blocks;

Converted into bytes, you have

  • info.st_size is the file size in bytes
  • stored_blocks*info.st_blksize is the number of bytes used on disk
  • sparse_blocks*info.st_blksize is the number of bytes in sparse holes on disk
  • zeroed_blocks*info.st_blksize is the number of unnecessarily stored zero bytes on disk; that could have been stored as a sparse hole instead

Note that you can use cp --sparse=always --preserve=all SOURCEFILE TARGETFILE to create an identical copy of a file, but "optimizing" sparseness, so that sufficiently long runs of zero bytes are stored as holes instead; this may help you in testing your program. See man 1 cp for details. You can also create long sequences of zeros by using dd if=/dev/zero of=TARGETFILE bs=BLOCKSIZE count=BLOCKS; see man 1 dd and man 4 null for details.


Edited to add:

Here is an example function, examine(), that opens the specified file, obtains the statistics, and if necessary (i.e. the amount of unnecessarily stored zeroes is requested), reads the entire file.

I have only lightly tested it, but it should implement the logic above.

It is very rough; I paid most attention to correct error checking and correctness of dynamic memory allocation/freeing. (It should check for and return all error conditions, even some should-never-occur ones, and never leak memory. That is, unless I have a bug or thinko in the code -- corrections are most welcome.)

It would be better to split it into smaller, easier to manage functions.

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

/* Return nonzero if the buffer is all zeros.
*/
static inline int is_zero(const void *const ptr, const size_t len)
{
    const char       *p = (const char *)ptr;
    const char *const q = (const char *const)ptr + len;

    while (p < q)
        if (*(p++))
            return 0;

    return 1;
}

/* Return 0 if success, errno error code otherwise.
 *   (*sizeptr):       File size in bytes
 *   (*blocksizeptr):  File block size in bytes
 *   (*storedptr):     Bytes stored on disk
 *   (*sparseptr):     Bytes in sparse holes
 *   (*zeroedptr):     Unnecessarily stored zero bytes
 * If zeroedptr is NULL, the file is only opened and
 * statistics obtained via fstat(). Otherwise, the entire
 * file will be read.
 * Special errors:
 *   EINVAL: NULL or empty file name
 *   EISDIR: Name refers to a directory
 *   EISNAM: Name refers to a pipe or device
 *   EBUSY:  File was modified during read
*/
int examine(const char *const filename,
            uint64_t *const sizeptr,
            uint64_t *const blocksizeptr,
            uint64_t *const storedptr,
            uint64_t *const sparseptr,
            uint64_t *const zeroedptr)
{
    struct stat  info;
    int          fd, result;
    size_t       size, have;
    uint64_t     total, nonzero, stored;
    int          cause = 0;
    char        *data = NULL;

    /* Check for NULL or empty filename. */
    if (!filename || !*filename)
        return errno = EINVAL;

    /* Open the specified file. */
    do {
        fd = open(filename, O_RDONLY | O_NOCTTY);
    } while (fd == -1 && errno == EINTR);
    if (fd == -1)
        return errno;

    do {

        /* Obtain file statistics. */
        if (fstat(fd, &info) == -1) {
            cause = errno;
            break;
        }

        /* Count total, rounding up to next multiple of block size. */
        total = (uint64_t)info.st_size;
        if (total % (uint64_t)info.st_blksize)
            total += (uint64_t)info.st_blksize - ((uint64_t)total % (uint64_t)info.st_blksize);
        /* Count total stored bytes. */
        stored = (uint64_t)512 * (uint64_t)info.st_blocks;

        /* Fill in immediately known fields. */
        if (sizeptr)
            *sizeptr = (uint64_t)info.st_size;
        if (blocksizeptr)
            *blocksizeptr = (uint64_t)info.st_blksize;
        if (storedptr)
            *storedptr = stored;
        if (sparseptr) {
            if (total > stored)
                *sparseptr = total - stored;
            else
                *sparseptr = 0;
        }
        if (zeroedptr)
            *zeroedptr = 0;

        /* Verify we have a regular file. */
        if (S_ISDIR(info.st_mode)) {
            cause = EISDIR;
            break;
        } else
        if (!S_ISREG(info.st_mode)) {
            cause = EISNAM;
            break;
        }

        /* Verify we have a valid block size. */
        if (info.st_blksize < (blksize_t)1) {
            cause = ENOTSUP;
            break;
        }

        /* If zeroedptr is NULL, we do not need to read the file. */
        if (!zeroedptr) {
            /* Close descriptor and return success. */
            do {
                result = close(fd);
            } while (result == -1 && errno == EINTR);
            if (result == -1)
                return errno;
            return 0;
        }

        /* Use large enough chunks for I/O. */
        if (info.st_blksize < (blksize_t)131072) {
            const size_t chunks = (size_t)131072 / (size_t)info.st_blksize;
            size = chunks * (size_t)info.st_blksize;
        } else
            size = (size_t)info.st_blksize;

        /* Allocate buffer. */
        data = malloc(size);
        if (!data) {
            cause = ENOMEM;
            break;
        }

        /* Clear counters. */
        total = 0;
        nonzero = 0;
        have = 0;

        /* Read loop. */
        while (1) {
            size_t  i;
            ssize_t bytes;
            int     ended = 0;

            while (have < (size_t)info.st_blksize) {

                bytes = read(fd, data + have, size - have);
                if (bytes > (ssize_t)0) {
                    have += bytes;
                    total += (uint64_t)bytes;
                } else
                if (bytes == (ssize_t)0) {
                    /* Clear the end of the buffer; just to be sure */
                    memset(data + have, 0, size - have);
                    ended = 1;
                    break;
                } else
                if (bytes != (ssize_t)-1) {
                    cause = EIO;
                    break;
                } else
                if (errno != EINTR) {
                    cause = errno;
                    break;
                }
            }

            if (cause)
                break;

            /* Count number of zero/nonzero chunks in buffer, but add up as bytes. */
            i = have / (size_t)info.st_blksize;
            while (i-->0)
                if (!is_zero(data + i * (size_t)info.st_blksize, (size_t)info.st_blksize))
                    nonzero += (uint64_t)info.st_blksize;

            /* Followed by a partial chunk? */
            {   const size_t overlap = have % (size_t)info.st_blksize;
                if (overlap) {
                    if (have > overlap)
                        memcpy(data, data + have - overlap, overlap);
                    have = overlap;
                } else
                    have = 0;
            }

            /* Next round of the loop, unless end of input. */
            if (!ended)
                continue;

            /* Entire file has been processed. */

            /* Partial chunk in buffer? */
            if (have) {
                if (!is_zero(data, have))
                    nonzero += (uint64_t)info.st_blksize;
            }

            /* If file size changed, update statistics. */
            if (total != (uint64_t)info.st_size) {
                if (fstat(fd, &info) == -1) {
                    cause = errno;
                    break;
                }
                /* File changed from under us? */
                if (total != (uint64_t)info.st_size) {
                    cause = EBUSY;
                    break;
                }
            }

            /* Align total size to (next) multiple of block size. */
            if (total % (uint64_t)info.st_blksize)
                total += (uint64_t)info.st_blksize - (total % (uint64_t)info.st_blksize);

            /* Bytes stored on disk. */
            stored = (uint64_t)512 * (uint64_t)info.st_blocks;

            /* Sanity check. (File changed while we read it?) */
            if (stored > total || nonzero > stored) {
                cause = EBUSY;
                break;
            }

            /* Update fields. */
            if (sizeptr)
                *sizeptr = (uint64_t)info.st_size;
            if (storedptr)
                *storedptr = (uint64_t)512 * (uint64_t)info.st_blocks;
            if (sparseptr)
                *sparseptr = total - stored;
            if (zeroedptr)
                *zeroedptr = (total - nonzero) - (total - stored);

            /* Discard buffer. */
            free(data);

            /* Close file and return. */
            do {
                result = close(fd);
            } while (result == -1 && errno == EINTR);
            if (result == -1)
                return errno;
            return 0;
        }
    } while (0);

    /* Free buffer, if allocated. free(NULL) is safe. */
    free(data);

    /* Close file, and return with cause. */
    do {
        result = close(fd);
    } while (result == -1 && errno == EINTR);
    return errno = cause;
}

For portability, all the return parameters are 64-bit unsigned integers, and specify the corresponding sizes in bytes. Note that (*storedptr)+(*sparseptr) defines the total number of bytes rounded up to next multiple of (*blocksizeptr). (*zeroesptr) only includes explicitly stored zeroes, not sparse holes. Again, think of (*zeroesptr) as the number of unnecessarily stored zeroes.

I used rm -f test ; dd if=/dev/zero of=test bs=10000 seek=3 count=1 to generate a test file with a 30,000 byte hole followed by 10,000 zeroes. examine() returns size=40000, blocksize=4096, stored=12288, sparse=28672, zeroed=12288, which seems correct to me.

Questions?

Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • Hi, I did try your suggestion. But I probably used the things above wrong, because I don't get the results I'd like to have and also I got once a segmentation fault. Could you maybe show me in the code above, if it's not to much hassle for you, how to modify the code above with your suggestions? –  Feb 02 '14 at 00:33
  • @user2965601: No, I won't do your homework for you. However, I did add an example implementation of an `examine()` function, that examines the named file, returning the numeric values needed for your output. I hope you take the time to read and understand it -- feel free to ask if there is something strange you don't understand --, and write your own code instead. Based on your code snippet, you have the necessary skills already. – Nominal Animal Feb 02 '14 at 16:28