Skip to content

Commit 4baaf13

Browse files
RomaLzhihKernel Patches Daemon
authored andcommitted
selftests/bpf: add test for blkcg io.stat BPF kfuncs
Add cgroup_iter_io, a test_progs test for the block I/O controller BPF kfuncs. A SEC("iter.s/cgroup") program acquires the cgroup's blkcg, flushes stats, iterates its blkgs and reads the io.stat counters for a target device. The userspace side attaches a loop device, generates O_DIRECT read and write I/O charged to a test cgroup, and then: - checks the write and read byte/io counters are nonzero, - checks the reported device id, - compares every kfunc-read value against the cgroup's io.stat file for the same device and requires an exact match, - reads the same device through bpf_get_root_blkcg() and checks the root counters are at or above the test cgroup's. The measured device is pinned to the loop device, which has no asynchronous writeback, so the kfunc snapshot and the io.stat file snapshot are identical rather than merely close. The root cgroup's numbers for a device come from the disk itself and so cover every cgroup's I/O to it, which is why the root check is "at or above" rather than an exact match. CONFIG_BLK_CGROUP is added to the test config; CONFIG_BLK_DEV_LOOP is already present. Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
1 parent 7663796 commit 4baaf13

4 files changed

Lines changed: 435 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* SPDX-License-Identifier: GPL-2.0 */
2+
/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
3+
#ifndef __CGROUP_ITER_IO_H
4+
#define __CGROUP_ITER_IO_H
5+
6+
struct io_query {
7+
/* one device's io.stat counters */
8+
__u64 rbytes;
9+
__u64 wbytes;
10+
__u64 rios;
11+
__u64 wios;
12+
__u64 dbytes;
13+
__u64 dios;
14+
__u64 dev; /* dev_t of the device the counters belong to */
15+
};
16+
17+
#endif /* __CGROUP_ITER_IO_H */

tools/testing/selftests/bpf/config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
CONFIG_BLK_CGROUP=y
12
CONFIG_BLK_DEV_LOOP=y
23
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
34
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=1
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
3+
#define _GNU_SOURCE
4+
#include <test_progs.h>
5+
#include <bpf/libbpf.h>
6+
#include <fcntl.h>
7+
#include <linux/loop.h>
8+
#include <stdlib.h>
9+
#include <string.h>
10+
#include <sys/ioctl.h>
11+
#include <sys/stat.h>
12+
#include <sys/sysmacros.h>
13+
#include <unistd.h>
14+
#include "cgroup_helpers.h"
15+
#include "cgroup_iter_io.h"
16+
#include "cgroup_iter_io.skel.h"
17+
18+
#define IO_SIZE (4 * 1024 * 1024)
19+
20+
static int read_stats(struct bpf_link *link)
21+
{
22+
int fd, ret = 0;
23+
ssize_t bytes;
24+
25+
fd = bpf_iter_create(bpf_link__fd(link));
26+
if (!ASSERT_OK_FD(fd, "bpf_iter_create"))
27+
return 1;
28+
29+
/* Results land in skel->data_query; the read itself returns no data. */
30+
bytes = read(fd, NULL, 0);
31+
if (!ASSERT_EQ(bytes, 0, "read fd"))
32+
ret = 1;
33+
34+
close(fd);
35+
return ret;
36+
}
37+
38+
/*
39+
* Attach a loop device to an anonymous temp file so we have a real block
40+
* device to generate cgroup-charged I/O against. Returns 0 on success, or -1
41+
* if loop devices are unavailable (non-root / no CONFIG_BLK_DEV_LOOP) so the
42+
* caller can skip.
43+
*/
44+
static int loop_setup(char *loop_path, size_t sz, int *ctl_fd, int *loop_fd,
45+
int *back_fd)
46+
{
47+
char back_path[] = "/tmp/cgroup_iter_io.XXXXXX";
48+
int nr;
49+
50+
*ctl_fd = *loop_fd = *back_fd = -1;
51+
52+
*ctl_fd = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
53+
if (*ctl_fd < 0)
54+
return -1;
55+
56+
nr = ioctl(*ctl_fd, LOOP_CTL_GET_FREE);
57+
if (nr < 0)
58+
goto err;
59+
snprintf(loop_path, sz, "/dev/loop%d", nr);
60+
61+
*back_fd = mkstemp(back_path);
62+
if (*back_fd < 0)
63+
goto err;
64+
unlink(back_path);
65+
if (ftruncate(*back_fd, (off_t)IO_SIZE * 4))
66+
goto err;
67+
68+
*loop_fd = open(loop_path, O_RDWR | O_CLOEXEC);
69+
if (*loop_fd < 0)
70+
goto err;
71+
if (ioctl(*loop_fd, LOOP_SET_FD, *back_fd))
72+
goto err;
73+
74+
return 0;
75+
err:
76+
if (*loop_fd >= 0)
77+
close(*loop_fd);
78+
if (*back_fd >= 0)
79+
close(*back_fd);
80+
close(*ctl_fd);
81+
*ctl_fd = *loop_fd = *back_fd = -1;
82+
return -1;
83+
}
84+
85+
static void loop_teardown(const char *loop_path, int ctl_fd, int loop_fd,
86+
int back_fd)
87+
{
88+
int nr = -1;
89+
90+
if (loop_fd >= 0) {
91+
ioctl(loop_fd, LOOP_CLR_FD, 0);
92+
close(loop_fd);
93+
}
94+
if (back_fd >= 0)
95+
close(back_fd);
96+
if (ctl_fd >= 0) {
97+
if (sscanf(loop_path, "/dev/loop%d", &nr) == 1 && nr >= 0)
98+
ioctl(ctl_fd, LOOP_CTL_REMOVE, nr);
99+
close(ctl_fd);
100+
}
101+
}
102+
103+
/* O_DIRECT I/O to the loop device, charged to the current cgroup. */
104+
static int do_direct_io(const char *loop_path)
105+
{
106+
void *buf;
107+
int fd, ret = -1;
108+
109+
fd = open(loop_path, O_RDWR | O_DIRECT | O_CLOEXEC);
110+
if (fd < 0)
111+
return -1;
112+
if (posix_memalign(&buf, 4096, IO_SIZE))
113+
goto out_fd;
114+
memset(buf, 0xab, IO_SIZE);
115+
116+
if (pwrite(fd, buf, IO_SIZE, 0) != IO_SIZE)
117+
goto out_buf;
118+
fsync(fd);
119+
if (pread(fd, buf, IO_SIZE, 0) != IO_SIZE)
120+
goto out_buf;
121+
ret = 0;
122+
out_buf:
123+
free(buf);
124+
out_fd:
125+
close(fd);
126+
return ret;
127+
}
128+
129+
/*
130+
* Parse the io.stat line for device @dev out of the cgroup's io.stat file and
131+
* fill @out. @dev is a kernel dev_t (as returned by bpf_blkg_dev), whose
132+
* major:minor split matches how io.stat prints the device. Returns 0 if the
133+
* device's line was found.
134+
*/
135+
static int parse_io_stat(int cgroup_fd, __u64 dev, struct io_query *out)
136+
{
137+
unsigned int want_maj = dev >> 20, want_min = dev & ((1U << 20) - 1);
138+
char buf[4096], *line, *saveptr;
139+
int fd, n, ret = -1;
140+
141+
fd = openat(cgroup_fd, "io.stat", O_RDONLY);
142+
if (fd < 0)
143+
return -1;
144+
n = read(fd, buf, sizeof(buf) - 1);
145+
close(fd);
146+
if (n <= 0)
147+
return -1;
148+
buf[n] = '\0';
149+
150+
for (line = strtok_r(buf, "\n", &saveptr); line;
151+
line = strtok_r(NULL, "\n", &saveptr)) {
152+
unsigned long long rb = 0, wb = 0, ri = 0, wi = 0, db = 0, di = 0;
153+
unsigned int maj, min;
154+
155+
/*
156+
* The "maj:min" token is always present; the field block is
157+
* optional (the kernel omits it for a device with no read/write
158+
* I/O), so a match of >= 2 is enough and absent fields stay 0.
159+
*/
160+
if (sscanf(line,
161+
"%u:%u rbytes=%llu wbytes=%llu rios=%llu wios=%llu dbytes=%llu dios=%llu",
162+
&maj, &min, &rb, &wb, &ri, &wi, &db, &di) < 2)
163+
continue;
164+
if (maj != want_maj || min != want_min)
165+
continue;
166+
167+
out->rbytes = rb;
168+
out->wbytes = wb;
169+
out->rios = ri;
170+
out->wios = wi;
171+
out->dbytes = db;
172+
out->dios = di;
173+
ret = 0;
174+
break;
175+
}
176+
return ret;
177+
}
178+
179+
void test_cgroup_iter_io(void)
180+
{
181+
char *cgroup_rel_path = "/cgroup_iter_io_test";
182+
int ctl_fd = -1, loop_fd = -1, back_fd = -1;
183+
struct cgroup_iter_io *skel = NULL;
184+
struct bpf_link *link = NULL;
185+
char loop_path[64];
186+
struct io_query *q;
187+
int cgroup_fd;
188+
189+
cgroup_fd = cgroup_setup_and_join(cgroup_rel_path);
190+
if (!ASSERT_OK_FD(cgroup_fd, "cgroup_setup_and_join"))
191+
return;
192+
193+
if (loop_setup(loop_path, sizeof(loop_path), &ctl_fd, &loop_fd, &back_fd)) {
194+
test__skip(); /* needs root + CONFIG_BLK_DEV_LOOP */
195+
goto cleanup_cgroup_fd;
196+
}
197+
198+
skel = cgroup_iter_io__open_and_load();
199+
if (!ASSERT_OK_PTR(skel, "cgroup_iter_io__open_and_load"))
200+
goto cleanup_loop;
201+
202+
/*
203+
* Pin the read to the loop device so the measured device is stable and
204+
* quiesced. Convert the glibc-encoded st_rdev to the kernel dev_t
205+
* encoding (major << 20 | minor) that bpf_blkg_dev returns.
206+
*/
207+
{
208+
struct stat lst;
209+
210+
if (!ASSERT_OK(fstat(loop_fd, &lst), "fstat loop"))
211+
goto cleanup_skel;
212+
skel->data_query->target_dev =
213+
((__u64)major(lst.st_rdev) << 20) | minor(lst.st_rdev);
214+
}
215+
216+
DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
217+
union bpf_iter_link_info linfo = {
218+
.cgroup.cgroup_fd = cgroup_fd,
219+
.cgroup.order = BPF_CGROUP_ITER_SELF_ONLY,
220+
};
221+
opts.link_info = &linfo;
222+
opts.link_info_len = sizeof(linfo);
223+
224+
link = bpf_program__attach_iter(skel->progs.cgroup_io_query, &opts);
225+
if (!ASSERT_OK_PTR(link, "bpf_program__attach_iter"))
226+
goto cleanup_skel;
227+
228+
/* This process is in the test cgroup, so the loop I/O is charged here. */
229+
if (!ASSERT_OK(do_direct_io(loop_path), "do_direct_io"))
230+
goto cleanup_link;
231+
232+
if (!ASSERT_OK(read_stats(link), "read stats"))
233+
goto cleanup_link;
234+
235+
/*
236+
* Weak check: we did I/O, so the numbers must be non-zero. Follows the
237+
* pattern in cgroup_iter_memcg.
238+
*/
239+
q = &skel->data_query->io_query;
240+
if (test__start_subtest("cgroup_iter_io__write")) {
241+
ASSERT_GT(q->wbytes, 0, "wbytes");
242+
ASSERT_GT(q->wios, 0, "wios");
243+
}
244+
if (test__start_subtest("cgroup_iter_io__read")) {
245+
ASSERT_GT(q->rbytes, 0, "rbytes");
246+
ASSERT_GT(q->rios, 0, "rios");
247+
}
248+
if (test__start_subtest("cgroup_iter_io__dev"))
249+
ASSERT_GT(q->dev, 0, "dev");
250+
251+
/*
252+
* Stronger check: the kfunc-read values must equal what the io.stat
253+
* file reports for the same device. Refresh via the prog, then read
254+
* the file with no I/O in between, so both flushed snapshots match
255+
* exactly.
256+
*/
257+
if (test__start_subtest("cgroup_iter_io__match")) {
258+
struct io_query filev = {};
259+
260+
if (ASSERT_OK(read_stats(link), "read stats") &&
261+
ASSERT_OK(parse_io_stat(cgroup_fd, q->dev, &filev),
262+
"parse io.stat")) {
263+
ASSERT_EQ(q->rbytes, filev.rbytes, "rbytes");
264+
ASSERT_EQ(q->wbytes, filev.wbytes, "wbytes");
265+
ASSERT_EQ(q->rios, filev.rios, "rios");
266+
ASSERT_EQ(q->wios, filev.wios, "wios");
267+
ASSERT_EQ(q->dbytes, filev.dbytes, "dbytes");
268+
ASSERT_EQ(q->dios, filev.dios, "dios");
269+
}
270+
}
271+
272+
/*
273+
* Separate program for the root block cgroup. Its counters do not come
274+
* from rstat, they are refilled from the disks themselves, so this
275+
* covers the other half of bpf_blkcg_flush_stats(). They cover every
276+
* cgroup's I/O to the loop device, and only this test touches it, so
277+
* they must be at or above what the test cgroup was charged.
278+
*/
279+
if (test__start_subtest("cgroup_iter_io__root")) {
280+
struct bpf_link *root_link;
281+
struct io_query *r;
282+
283+
skel->data_query->got_root_blkcg = 0;
284+
root_link = bpf_program__attach_iter(skel->progs.cgroup_root_blkcg_query,
285+
&opts);
286+
if (ASSERT_OK_PTR(root_link, "attach root iter")) {
287+
if (ASSERT_OK(read_stats(root_link), "read root stats")) {
288+
r = &skel->data_query->root_query;
289+
ASSERT_EQ(skel->data_query->got_root_blkcg, 1,
290+
"got_root_blkcg");
291+
ASSERT_EQ(r->dev, q->dev, "root dev");
292+
ASSERT_GE(r->wbytes, q->wbytes, "root wbytes");
293+
ASSERT_GE(r->wios, q->wios, "root wios");
294+
ASSERT_GE(r->rbytes, q->rbytes, "root rbytes");
295+
ASSERT_GE(r->rios, q->rios, "root rios");
296+
}
297+
bpf_link__destroy(root_link);
298+
}
299+
}
300+
301+
cleanup_link:
302+
bpf_link__destroy(link);
303+
cleanup_skel:
304+
cgroup_iter_io__destroy(skel);
305+
cleanup_loop:
306+
loop_teardown(loop_path, ctl_fd, loop_fd, back_fd);
307+
cleanup_cgroup_fd:
308+
close(cgroup_fd);
309+
cleanup_cgroup_environment();
310+
}

0 commit comments

Comments
 (0)