1

Running on Tumbleweed with kernel 6.2.12-1-default

Using the _GNU_SOURCE define should allow the use of SEEK_DATA as the whence value for lseek.

According to the man page this should work for a range of file systems. I have tested this code on btrfs and ext4 with the same result.

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

void test(char *filepath) {
    int write_value = 0x22;

    int fh = open(filepath, O_CREAT | O_TRUNC | O_RDWR);

    int os    = lseek(fh, 10, SEEK_SET);
    int wret  = write(fh, &write_value, sizeof(int));
    int os2   = lseek(fh, 0, SEEK_DATA);
    printf("os: %d os2: %d wret %d\n", os, os2, wret);
    close(fh);
}

Given the above code, I would expect SEEK_DATA to find the first written value at offset 10, and that os2 == os, yet lseek returns 0.

The file is indeed written as expected and od -x gives the following output:

0000000 0000 0000 0000 0000 0000 0022 0000

Any suggestions? Have I made an incorrect assumption about the expected behaviour...

Rashid
  • 41
  • 2
  • Possible dup https://stackoverflow.com/q/18273773/1216776 – stark May 02 '23 at 17:57
  • I had read that post but this is 10 years later, and I was already aware of the need for #define _GNU_SOURCE. No solution seemed to be forthcoming then... – Rashid May 02 '23 at 18:06
  • Shouldn't hole be at least one filesystem block in size? – dimich May 02 '23 at 23:50
  • Thanks, that's interesting, it seems that there is some truth in what you say. With the above code set to different values, the SEEK_DATA does indeed return the beginning of the block that contains the data: 4095 = 0 4096 = 4096 4097 = 4096 8191 = 4096 8192 = 8192 8193 = 8192 However this is not clear from the man page: _Adjust the file offset to the next location in the file greater than or equal to offset containing data. If offset points to data, then the file offset is set to offset._ Is this really the intended behaviour? – Rashid May 03 '23 at 09:27
  • Could you please show updated code which gives results " 4095 = 0 4096 = 4096 ..."? What offsets to `lseek()`s with SEEK_SET and SEEK_DATA are passed and how many bytes written with `write()? – dimich May 03 '23 at 15:27
  • The code is identical, other than changing the lseek value from 10: `int os = lseek(fh, 10, SEEK_SET);` – Rashid May 03 '23 at 16:20
  • results: seek du -b 4095 = 4099 4096 = 4100 4097 = 4101 8191 = 8195 9182 = 8196 These seem correct, the specified offset + 4 bytes for the integer written. – Rashid May 03 '23 at 16:30
  • Pls see the answer below. – dimich May 04 '23 at 00:15

1 Answers1

0

Sparse files are implementation-specific. We can assume hole granularity depends on filesystem I/O block size.

I modified your program to make some tests:

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdint.h>

static void test(off_t offset)
{
    int write_value = 0x22;

    int fh = open("dummy", O_CREAT | O_TRUNC | O_RDWR, 0644);

    off_t os     = lseek(fh, offset, SEEK_SET);
    ssize_t wret = write(fh, &write_value, sizeof(write_value));
    off_t os2    = lseek(fh, 0, SEEK_DATA);

    struct stat statbuf;
    int ret = fstat(fh, &statbuf);

    printf("% 6jd | % 6jd | % 6zd | % 6jd\n",
        (intmax_t)os, (intmax_t)os2, wret,
        (intmax_t)(ret == 0 ? statbuf.st_blocks*512 : -1));

    close(fh);

    unlink("dummy");
}

int main(int, char **argv)
{
    printf("   os  |   os2  |  wret  |  used \n"
           "----------------------------------\n");

    while (*++argv)
    {
        test(atol(*argv));
    }

    return 0;
}

Filesystem block size is usual:

$ stat -f -c "%s" .
4096

Results:

$ ./holes 10 4095 4096 8191 8192 8193
   os  |   os2  |  wret  |  used 
----------------------------------
    10 |      0 |      4 |   4096
  4095 |      0 |      4 |   8192
  4096 |   4096 |      4 |   4096
  8191 |   4096 |      4 |   8192
  8192 |   8192 |      4 |   4096
  8193 |   8192 |      4 |   4096

Analysis:

Write offset 10: | data |

0 blocks of hole, 1 block of data. Data block starts at the beginning: os2 == 0.

Write offset 4095: | data | data |

0 blocks of hole, 2 blocks of data. Data block starts at the beginning: os2 == 0.

Write offset 4096: | hole | data |

1 block of hole, 1 block of data. Data block start from second, os2 == 4096.

Write offset 8191: | hole | data | data |

1 block of hole, 2 blocks of data. Data blocks start from second, os2 == 4096

Write offset 8192: | hole | hole | data |

2 blocks of hole, 1 blocks of data. Data blocks start from third, os2 == 8192

Write offset 8193: the same as 8192.

Observed results meet expected.

dimich
  • 1,305
  • 1
  • 5
  • 7
  • Great mods (and more accurate types!) I was just hacking the code for different test values ;) I still think the man pages should be far clearer about what the SEEK_DATA actually returns. There is no mention of blocks... – Rashid May 04 '23 at 10:19
  • @Rashid Indeed types weren't correct, i.e. `off_t` may be wider than `ssize_t`. It's not affect small values but i updated the answer. Regarding documentation, it is said that *SEEK_DATA and SEEK_HOLE are nonstandard extensions*, as well as *a filesystem is not obliged to report holes, so these operations are not a guaranteed mechanism for mapping the storage space actually allocated to a file*. There is mention in [man 1 fallocate](https://man7.org/linux/man-pages/man1/fallocate.1.html): *The minimum size of the hole depends on filesystem I/O block size (usually 4096 bytes)*. – dimich May 04 '23 at 16:28