root/tools/testing/selftests/cachestat/test_cachestat.c
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__ // Use ll64

#include <stdio.h>
#include <stdbool.h>
#include <linux/kernel.h>
#include <linux/magic.h>
#include <linux/mman.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <sys/syscall.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>

#include "kselftest.h"

#define NR_TESTS        9

static const char * const dev_files[] = {
        "/dev/zero", "/dev/null", "/dev/urandom",
        "/proc/version", "/proc"
};

void print_cachestat(struct cachestat *cs)
{
        ksft_print_msg(
        "Using cachestat: Cached: %llu, Dirty: %llu, Writeback: %llu, Evicted: %llu, Recently Evicted: %llu\n",
        cs->nr_cache, cs->nr_dirty, cs->nr_writeback,
        cs->nr_evicted, cs->nr_recently_evicted);
}

enum file_type {
        FILE_MMAP,
        FILE_SHMEM
};

bool write_exactly(int fd, size_t filesize)
{
        int random_fd = open("/dev/urandom", O_RDONLY);
        char *cursor, *data;
        int remained;
        bool ret;

        if (random_fd < 0) {
                ksft_print_msg("Unable to access urandom.\n");
                ret = false;
                goto out;
        }

        data = malloc(filesize);
        if (!data) {
                ksft_print_msg("Unable to allocate data.\n");
                ret = false;
                goto close_random_fd;
        }

        remained = filesize;
        cursor = data;

        while (remained) {
                ssize_t read_len = read(random_fd, cursor, remained);

                if (read_len <= 0) {
                        ksft_print_msg("Unable to read from urandom.\n");
                        ret = false;
                        goto out_free_data;
                }

                remained -= read_len;
                cursor += read_len;
        }

        /* write random data to fd */
        remained = filesize;
        cursor = data;
        while (remained) {
                ssize_t write_len = write(fd, cursor, remained);

                if (write_len <= 0) {
                        ksft_print_msg("Unable write random data to file.\n");
                        ret = false;
                        goto out_free_data;
                }

                remained -= write_len;
                cursor += write_len;
        }

        ret = true;
out_free_data:
        free(data);
close_random_fd:
        close(random_fd);
out:
        return ret;
}

/*
 * fsync() is implemented via noop_fsync() on tmpfs. This makes the fsync()
 * test fail below, so we need to check for test file living on a tmpfs.
 */
static bool is_on_tmpfs(int fd)
{
        struct statfs statfs_buf;

        if (fstatfs(fd, &statfs_buf))
                return false;

        return statfs_buf.f_type == TMPFS_MAGIC;
}

/*
 * Open/create the file at filename, (optionally) write random data to it
 * (exactly num_pages), then test the cachestat syscall on this file.
 *
 * If test_fsync == true, fsync the file, then check the number of dirty
 * pages.
 */
static int test_cachestat(const char *filename, bool write_random, bool create,
                          bool test_fsync, unsigned long num_pages,
                          int open_flags, mode_t open_mode)
{
        size_t PS = sysconf(_SC_PAGESIZE);
        int filesize = num_pages * PS;
        int ret = KSFT_PASS;
        long syscall_ret;
        struct cachestat cs;
        struct cachestat_range cs_range = { 0, filesize };

        int fd = open(filename, open_flags, open_mode);

        if (fd == -1) {
                ksft_print_msg("Unable to create/open file.\n");
                ret = KSFT_FAIL;
                goto out;
        } else {
                ksft_print_msg("Create/open %s\n", filename);
        }

        if (write_random) {
                if (!write_exactly(fd, filesize)) {
                        ksft_print_msg("Unable to access urandom.\n");
                        ret = KSFT_FAIL;
                        goto out1;
                }
        }

        syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

        ksft_print_msg("Cachestat call returned %ld\n", syscall_ret);

        if (syscall_ret) {
                ksft_print_msg("Cachestat returned non-zero.\n");
                ret = KSFT_FAIL;
                goto out1;

        } else {
                print_cachestat(&cs);

                if (write_random) {
                        if (cs.nr_cache + cs.nr_evicted != num_pages) {
                                ksft_print_msg(
                                        "Total number of cached and evicted pages is off.\n");
                                ret = KSFT_FAIL;
                        }
                }
        }

        if (test_fsync) {
                if (is_on_tmpfs(fd)) {
                        ret = KSFT_SKIP;
                } else if (fsync(fd)) {
                        ksft_print_msg("fsync fails.\n");
                        ret = KSFT_FAIL;
                } else {
                        syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

                        ksft_print_msg("Cachestat call (after fsync) returned %ld\n",
                                syscall_ret);

                        if (!syscall_ret) {
                                print_cachestat(&cs);

                                if (cs.nr_dirty) {
                                        ret = KSFT_FAIL;
                                        ksft_print_msg(
                                                "Number of dirty should be zero after fsync.\n");
                                }
                        } else {
                                ksft_print_msg("Cachestat (after fsync) returned non-zero.\n");
                                ret = KSFT_FAIL;
                                goto out1;
                        }
                }
        }

out1:
        close(fd);

        if (create)
                remove(filename);
out:
        return ret;
}
const char *file_type_str(enum file_type type)
{
        switch (type) {
        case FILE_SHMEM:
                return "shmem";
        case FILE_MMAP:
                return "mmap";
        default:
                return "unknown";
        }
}


bool run_cachestat_test(enum file_type type)
{
        size_t PS = sysconf(_SC_PAGESIZE);
        size_t filesize = PS * 512 * 2; /* 2 2MB huge pages */
        int syscall_ret;
        size_t compute_len = PS * 512;
        struct cachestat_range cs_range = { PS, compute_len };
        char *filename = "tmpshmcstat", *map;
        struct cachestat cs;
        bool ret = true;
        int fd;
        unsigned long num_pages = compute_len / PS;
        if (type == FILE_SHMEM)
                fd = shm_open(filename, O_CREAT | O_RDWR, 0600);
        else
                fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0666);

        if (fd < 0) {
                ksft_print_msg("Unable to create %s file.\n",
                                file_type_str(type));
                ret = false;
                goto out;
        }

        if (ftruncate(fd, filesize)) {
                ksft_print_msg("Unable to truncate %s file.\n",file_type_str(type));
                ret = false;
                goto close_fd;
        }
        switch (type) {
        case FILE_SHMEM:
                if (!write_exactly(fd, filesize)) {
                        ksft_print_msg("Unable to write to file.\n");
                        ret = false;
                        goto close_fd;
                }
                break;
        case FILE_MMAP:
                map = mmap(NULL, filesize, PROT_READ | PROT_WRITE,
                                 MAP_SHARED, fd, 0);

                if (map == MAP_FAILED) {
                        ksft_print_msg("mmap failed.\n");
                        ret = false;
                        goto close_fd;
                }
                for (int i = 0; i < filesize; i++)
                        map[i] = 'A';
                break;
        default:
                ksft_print_msg("Unsupported file type.\n");
                ret = false;
                goto close_fd;
        }
        syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

        if (syscall_ret) {
                ksft_print_msg("Cachestat returned non-zero.\n");
                ret = false;
                goto close_fd;
        } else {
                print_cachestat(&cs);
                if (cs.nr_cache + cs.nr_evicted != num_pages) {
                        ksft_print_msg(
                                "Total number of cached and evicted pages is off.\n");
                        ret = false;
                }
        }

close_fd:
        shm_unlink(filename);
out:
        return ret;
}

int main(void)
{
        int ret;

        ksft_print_header();

        ret = syscall(__NR_cachestat, -1, NULL, NULL, 0);
        if (ret == -1 && errno == ENOSYS)
                ksft_exit_skip("cachestat syscall not available\n");

        ksft_set_plan(NR_TESTS);

        if (ret == -1 && errno == EBADF) {
                ksft_test_result_pass("bad file descriptor recognized\n");
                ret = 0;
        } else {
                ksft_test_result_fail("bad file descriptor ignored\n");
                ret = 1;
        }

        for (int i = 0; i < 5; i++) {
                const char *dev_filename = dev_files[i];

                if (test_cachestat(dev_filename, false, false, false,
                        4, O_RDONLY, 0400) == KSFT_PASS)
                        ksft_test_result_pass("cachestat works with %s\n", dev_filename);
                else {
                        ksft_test_result_fail("cachestat fails with %s\n", dev_filename);
                        ret = 1;
                }
        }

        if (test_cachestat("tmpfilecachestat", true, true,
                false, 4, O_CREAT | O_RDWR, 0600) == KSFT_PASS)
                ksft_test_result_pass("cachestat works with a normal file\n");
        else {
                ksft_test_result_fail("cachestat fails with normal file\n");
                ret = 1;
        }

        switch (test_cachestat("tmpfilecachestat", true, true,
                true, 4, O_CREAT | O_RDWR, 0600)) {
        case KSFT_FAIL:
                ksft_test_result_fail("cachestat fsync fails with normal file\n");
                ret = KSFT_FAIL;
                break;
        case KSFT_PASS:
                ksft_test_result_pass("cachestat fsync works with a normal file\n");
                break;
        case KSFT_SKIP:
                ksft_test_result_skip("tmpfilecachestat is on tmpfs\n");
                break;
        }

        if (run_cachestat_test(FILE_SHMEM))
                ksft_test_result_pass("cachestat works with a shmem file\n");
        else {
                ksft_test_result_fail("cachestat fails with a shmem file\n");
                ret = 1;
        }

        if (run_cachestat_test(FILE_MMAP))
                ksft_test_result_pass("cachestat works with a mmap file\n");
        else {
                ksft_test_result_fail("cachestat fails with a mmap file\n");
                ret = 1;
        }
        return ret;
}