The cachestat() syscall reads page cache statistics straight from the file's f_mapping. Stackable filesystems such as overlayfs keep the data pages in an underlying inode's mapping rather than in the overlay inode's, so cachestat() reports all zeroes for them. Add a ->cachestat() file operation and route the syscall through a new vfs_cachestat() helper that calls it when present, falling back to file's f_mapping otherwise. This lets stackable filesystems forward the query to the file that actually owns the page cache. No behaviour change for regular files. Signed-off-by: Pavel Tikhomirov --- Note: Memset change might be a bit tricky, I moved it to no ->cachestat() path to avoid multiple memset on nested overlayfs, that means that ->cachestat() is expected to be able to handle unitialized cs. --- include/linux/fs.h | 10 ++++++++++ mm/filemap.c | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/include/linux/fs.h b/include/linux/fs.h index 6da44573ce450..966b6564707e4 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -53,6 +53,8 @@ struct bdi_writeback; struct bio; +struct cachestat_range; +struct cachestat; struct io_comp_batch; struct fiemap_extent_info; struct kiocb; @@ -1963,6 +1965,8 @@ struct file_operations { struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); + int (*cachestat)(struct file *file, struct cachestat_range *csr, + struct cachestat *cs); int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags); int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *, unsigned int poll_flags); @@ -3633,6 +3637,12 @@ extern int vfs_fadvise(struct file *file, loff_t offset, loff_t len, extern int generic_fadvise(struct file *file, loff_t offset, loff_t len, int advice); +/* mm/filemap.c */ +#ifdef CONFIG_CACHESTAT_SYSCALL +int vfs_cachestat(struct file *file, struct cachestat_range *csr, + struct cachestat *cs); +#endif + static inline bool vfs_empty_path(int dfd, const char __user *path) { char c; diff --git a/mm/filemap.c b/mm/filemap.c index 7e467c81d2138..90608c6b1ce55 100644 --- a/mm/filemap.c +++ b/mm/filemap.c @@ -4714,6 +4714,37 @@ static inline bool can_do_cachestat(struct file *f) return file_permission(f, MAY_WRITE) == 0; } +/** + * vfs_cachestat() - query page cache statistics of a file + * @file: file to query + * @csr: byte range to query + * @cs: output statistics + * + * Compute the page cache statistics for the given byte range of @file. + * + * Stackable filesystems (e.g. overlayfs) keep the data pages in the + * mapping of an underlying file rather than in @file->f_mapping. Such + * filesystems provide a ->cachestat() file operation that forwards the + * query to the file that actually owns the page cache; otherwise the + * statistics are computed from @file->f_mapping directly. + */ +int vfs_cachestat(struct file *file, struct cachestat_range *csr, + struct cachestat *cs) +{ + pgoff_t first_index, last_index; + + if (file->f_op->cachestat) + return file->f_op->cachestat(file, csr, cs); + + first_index = csr->off >> PAGE_SHIFT; + last_index = + csr->len == 0 ? ULONG_MAX : (csr->off + csr->len - 1) >> PAGE_SHIFT; + memset(cs, 0, sizeof(struct cachestat)); + filemap_cachestat(file->f_mapping, first_index, last_index, cs); + return 0; +} +EXPORT_SYMBOL(vfs_cachestat); + /* * The cachestat(2) system call. * @@ -4753,10 +4784,9 @@ SYSCALL_DEFINE4(cachestat, unsigned int, fd, struct cachestat __user *, cstat, unsigned int, flags) { CLASS(fd, f)(fd); - struct address_space *mapping; struct cachestat_range csr; struct cachestat cs; - pgoff_t first_index, last_index; + int ret; if (fd_empty(f)) return -EBADF; @@ -4775,12 +4805,9 @@ SYSCALL_DEFINE4(cachestat, unsigned int, fd, if (flags != 0) return -EINVAL; - first_index = csr.off >> PAGE_SHIFT; - last_index = - csr.len == 0 ? ULONG_MAX : (csr.off + csr.len - 1) >> PAGE_SHIFT; - memset(&cs, 0, sizeof(struct cachestat)); - mapping = fd_file(f)->f_mapping; - filemap_cachestat(mapping, first_index, last_index, &cs); + ret = vfs_cachestat(fd_file(f), &csr, &cs); + if (ret) + return ret; if (copy_to_user(cstat, &cs, sizeof(struct cachestat))) return -EFAULT; -- 2.54.0 Overlayfs forwards data I/O to the real (upper/lower) file, so the page cache lives in the real inode's mapping and cachestat() on an overlay fd returned all zeroes. Implement the ->cachestat() file operation by forwarding to the real file via vfs_cachestat(), the same way ovl_fadvise() forwards for fadvise. Signed-off-by: Pavel Tikhomirov --- fs/overlayfs/file.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c index 27cc07738f33b..a7e252a91ea43 100644 --- a/fs/overlayfs/file.c +++ b/fs/overlayfs/file.c @@ -518,6 +518,21 @@ static int ovl_fadvise(struct file *file, loff_t offset, loff_t len, int advice) return vfs_fadvise(realfile, offset, len, advice); } +#ifdef CONFIG_CACHESTAT_SYSCALL +static int ovl_cachestat(struct file *file, struct cachestat_range *csr, + struct cachestat *cs) +{ + struct file *realfile; + + realfile = ovl_real_file(file); + if (IS_ERR(realfile)) + return PTR_ERR(realfile); + + with_ovl_creds(file_inode(file)->i_sb) + return vfs_cachestat(realfile, csr, cs); +} +#endif + enum ovl_copyop { OVL_COPY, OVL_CLONE, @@ -642,6 +657,9 @@ const struct file_operations ovl_file_operations = { .mmap = ovl_mmap, .fallocate = ovl_fallocate, .fadvise = ovl_fadvise, +#ifdef CONFIG_CACHESTAT_SYSCALL + .cachestat = ovl_cachestat, +#endif .flush = ovl_flush, .splice_read = ovl_splice_read, .splice_write = ovl_splice_write, -- 2.54.0 Mount an overlayfs, create and write a file in the merged directory, and run cachestat() on it, reusing the existing test_cachestat() helper. Also bump NR_TESTS to the actual number of tests run: it was 9 while ten tests were already executed, and this adds an eleventh. Signed-off-by: Pavel Tikhomirov --- .../selftests/cachestat/test_cachestat.c | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/cachestat/test_cachestat.c b/tools/testing/selftests/cachestat/test_cachestat.c index 542cd09cb4434..1662d9817c50b 100644 --- a/tools/testing/selftests/cachestat/test_cachestat.c +++ b/tools/testing/selftests/cachestat/test_cachestat.c @@ -4,21 +4,25 @@ #include #include +#include #include #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include "kselftest.h" -#define NR_TESTS 9 +#define NR_TESTS 11 static const char * const dev_files[] = { "/dev/zero", "/dev/null", "/dev/urandom", @@ -294,6 +298,62 @@ bool run_cachestat_test(enum file_type type) return ret; } +/* + * Set up an overlayfs mount and run cachestat on a freshly created file in the + * merged directory. Overlayfs forwards data I/O to the underlying (upper) + * inode, so the page cache lives there and not in the overlay inode's mapping. + * This is a regression test for cachestat returning all zeroes on overlayfs. + */ +static int run_cachestat_overlayfs_test(void) +{ + char tmpl[] = "/tmp/cachestat_ovl.XXXXXX"; + char lower[PATH_MAX], upper[PATH_MAX], work[PATH_MAX]; + char merged[PATH_MAX], opts[4 * PATH_MAX], file[PATH_MAX]; + char *base; + int ret; + + base = mkdtemp(tmpl); + if (!base) { + ksft_print_msg("Unable to create overlayfs base dir: %s\n", + strerror(errno)); + return KSFT_FAIL; + } + + snprintf(lower, sizeof(lower), "%s/lower", base); + snprintf(upper, sizeof(upper), "%s/upper", base); + snprintf(work, sizeof(work), "%s/work", base); + snprintf(merged, sizeof(merged), "%s/merged", base); + + if (mkdir(lower, 0755) || mkdir(upper, 0755) || + mkdir(work, 0755) || mkdir(merged, 0755)) { + ksft_print_msg("Unable to create overlayfs dirs: %s\n", + strerror(errno)); + ret = KSFT_FAIL; + goto cleanup; + } + + snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s", + lower, upper, work); + + if (mount("overlay", merged, "overlay", 0, opts)) { + ksft_print_msg("Unable to mount overlayfs (need root?): %s\n", + strerror(errno)); + ret = KSFT_SKIP; + goto cleanup; + } + + snprintf(file, sizeof(file), "%s/merged/cachestat", base); + ret = test_cachestat(file, true, true, false, 4, O_CREAT | O_RDWR, 0600); + + umount(merged); +cleanup: + /* Best-effort recursive cleanup of the temporary tree. */ + snprintf(opts, sizeof(opts), "rm -rf %s", base); + if (system(opts)) + ksft_print_msg("Unable to clean up %s\n", base); + return ret; +} + int main(void) { int ret; @@ -361,5 +421,18 @@ int main(void) ksft_test_result_fail("cachestat fails with a mmap file\n"); ret = 1; } + + switch (run_cachestat_overlayfs_test()) { + case KSFT_FAIL: + ksft_test_result_fail("cachestat fails with an overlayfs file\n"); + ret = 1; + break; + case KSFT_PASS: + ksft_test_result_pass("cachestat works with an overlayfs file\n"); + break; + case KSFT_SKIP: + ksft_test_result_skip("overlayfs not available\n"); + break; + } return ret; } -- 2.54.0