1// SPDX-License-Identifier: GPL-2.0
2/*
3 * memcg_event_listener.c - Simple listener of memcg memory.events
4 *
5 * Copyright (c) 2023, SaluteDevices. All Rights Reserved.
6 *
7 * Author: Dmitry Rokosov <ddrokosov@salutedevices.com>
8 */
9
10#include <err.h>
11#include <errno.h>
12#include <limits.h>
13#include <poll.h>
14#include <stdbool.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include <string.h>
18#include <sys/inotify.h>
19#include <unistd.h>
20
21#define MEMCG_EVENTS "memory.events"
22
23/* Size of buffer to use when reading inotify events */
24#define INOTIFY_BUFFER_SIZE 8192
25
26#define INOTIFY_EVENT_NEXT(event, length) ({         \
27	(length) -= sizeof(*(event)) + (event)->len; \
28	(event)++;                                   \
29})
30
31#define INOTIFY_EVENT_OK(event, length) ((length) >= (ssize_t)sizeof(*(event)))
32
33#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
34
35struct memcg_counters {
36	long low;
37	long high;
38	long max;
39	long oom;
40	long oom_kill;
41	long oom_group_kill;
42};
43
44struct memcg_events {
45	struct memcg_counters counters;
46	char path[PATH_MAX];
47	int inotify_fd;
48	int inotify_wd;
49};
50
51static void print_memcg_counters(const struct memcg_counters *counters)
52{
53	printf("MEMCG events:\n");
54	printf("\tlow: %ld\n", counters->low);
55	printf("\thigh: %ld\n", counters->high);
56	printf("\tmax: %ld\n", counters->max);
57	printf("\toom: %ld\n", counters->oom);
58	printf("\toom_kill: %ld\n", counters->oom_kill);
59	printf("\toom_group_kill: %ld\n", counters->oom_group_kill);
60}
61
62static int get_memcg_counter(char *line, const char *name, long *counter)
63{
64	size_t len = strlen(name);
65	char *endptr;
66	long tmp;
67
68	if (memcmp(line, name, len)) {
69		warnx("Counter line %s has wrong name, %s is expected",
70		      line, name);
71		return -EINVAL;
72	}
73
74	/* skip the whitespace delimiter */
75	len += 1;
76
77	errno = 0;
78	tmp = strtol(&line[len], &endptr, 10);
79	if (((tmp == LONG_MAX || tmp == LONG_MIN) && errno == ERANGE) ||
80	    (errno && !tmp)) {
81		warnx("Failed to parse: %s", &line[len]);
82		return -ERANGE;
83	}
84
85	if (endptr == &line[len]) {
86		warnx("Not digits were found in line %s", &line[len]);
87		return -EINVAL;
88	}
89
90	if (!(*endptr == '\0' || (*endptr == '\n' && *++endptr == '\0'))) {
91		warnx("Further characters after number: %s", endptr);
92		return -EINVAL;
93	}
94
95	*counter = tmp;
96
97	return 0;
98}
99
100static int read_memcg_events(struct memcg_events *events, bool show_diff)
101{
102	FILE *fp = fopen(events->path, "re");
103	size_t i;
104	int ret = 0;
105	bool any_new_events = false;
106	char *line = NULL;
107	size_t len = 0;
108	struct memcg_counters new_counters;
109	struct memcg_counters *counters = &events->counters;
110	struct {
111		const char *name;
112		long *new;
113		long *old;
114	} map[] = {
115		{
116			.name = "low",
117			.new = &new_counters.low,
118			.old = &counters->low,
119		},
120		{
121			.name = "high",
122			.new = &new_counters.high,
123			.old = &counters->high,
124		},
125		{
126			.name = "max",
127			.new = &new_counters.max,
128			.old = &counters->max,
129		},
130		{
131			.name = "oom",
132			.new = &new_counters.oom,
133			.old = &counters->oom,
134		},
135		{
136			.name = "oom_kill",
137			.new = &new_counters.oom_kill,
138			.old = &counters->oom_kill,
139		},
140		{
141			.name = "oom_group_kill",
142			.new = &new_counters.oom_group_kill,
143			.old = &counters->oom_group_kill,
144		},
145	};
146
147	if (!fp) {
148		warn("Failed to open memcg events file %s", events->path);
149		return -EBADF;
150	}
151
152	/* Read new values for memcg counters */
153	for (i = 0; i < ARRAY_SIZE(map); ++i) {
154		ssize_t nread;
155
156		errno = 0;
157		nread = getline(&line, &len, fp);
158		if (nread == -1) {
159			if (errno) {
160				warn("Failed to read line for counter %s",
161				     map[i].name);
162				ret = -EIO;
163				goto exit;
164			}
165
166			break;
167		}
168
169		ret = get_memcg_counter(line, map[i].name, map[i].new);
170		if (ret) {
171			warnx("Failed to get counter value from line %s", line);
172			goto exit;
173		}
174	}
175
176	for (i = 0; i < ARRAY_SIZE(map); ++i) {
177		long diff;
178
179		if (*map[i].new > *map[i].old) {
180			diff = *map[i].new - *map[i].old;
181
182			if (show_diff)
183				printf("*** %ld MEMCG %s event%s, "
184				       "change counter %ld => %ld\n",
185				       diff, map[i].name,
186				       (diff == 1) ? "" : "s",
187				       *map[i].old, *map[i].new);
188
189			*map[i].old += diff;
190			any_new_events = true;
191		}
192	}
193
194	if (show_diff && !any_new_events)
195		printf("*** No new untracked memcg events available\n");
196
197exit:
198	free(line);
199	fclose(fp);
200
201	return ret;
202}
203
204static void process_memcg_events(struct memcg_events *events,
205				 struct inotify_event *event)
206{
207	int ret;
208
209	if (events->inotify_wd != event->wd) {
210		warnx("Unknown inotify event %d, should be %d", event->wd,
211		      events->inotify_wd);
212		return;
213	}
214
215	printf("Received event in %s:\n", events->path);
216
217	if (!(event->mask & IN_MODIFY)) {
218		warnx("No IN_MODIFY event, skip it");
219		return;
220	}
221
222	ret = read_memcg_events(events, /* show_diff = */true);
223	if (ret)
224		warnx("Can't read memcg events");
225}
226
227static void monitor_events(struct memcg_events *events)
228{
229	struct pollfd fds[1];
230	int ret;
231
232	printf("Started monitoring memory events from '%s'...\n", events->path);
233
234	fds[0].fd = events->inotify_fd;
235	fds[0].events = POLLIN;
236
237	for (;;) {
238		ret = poll(fds, ARRAY_SIZE(fds), -1);
239		if (ret < 0 && errno != EAGAIN)
240			err(EXIT_FAILURE, "Can't poll memcg events (%d)", ret);
241
242		if (fds[0].revents & POLLERR)
243			err(EXIT_FAILURE, "Got POLLERR during monitor events");
244
245		if (fds[0].revents & POLLIN) {
246			struct inotify_event *event;
247			char buffer[INOTIFY_BUFFER_SIZE];
248			ssize_t length;
249
250			length = read(fds[0].fd, buffer, INOTIFY_BUFFER_SIZE);
251			if (length <= 0)
252				continue;
253
254			event = (struct inotify_event *)buffer;
255			while (INOTIFY_EVENT_OK(event, length)) {
256				process_memcg_events(events, event);
257				event = INOTIFY_EVENT_NEXT(event, length);
258			}
259		}
260	}
261}
262
263static int initialize_memcg_events(struct memcg_events *events,
264				   const char *cgroup)
265{
266	int ret;
267
268	memset(events, 0, sizeof(struct memcg_events));
269
270	ret = snprintf(events->path, PATH_MAX,
271		       "/sys/fs/cgroup/%s/memory.events", cgroup);
272	if (ret >= PATH_MAX) {
273		warnx("Path to cgroup memory.events is too long");
274		return -EMSGSIZE;
275	} else if (ret < 0) {
276		warn("Can't generate cgroup event full name");
277		return ret;
278	}
279
280	ret = read_memcg_events(events, /* show_diff = */false);
281	if (ret) {
282		warnx("Failed to read initial memcg events state (%d)", ret);
283		return ret;
284	}
285
286	events->inotify_fd = inotify_init();
287	if (events->inotify_fd < 0) {
288		warn("Failed to setup new inotify device");
289		return -EMFILE;
290	}
291
292	events->inotify_wd = inotify_add_watch(events->inotify_fd,
293					       events->path, IN_MODIFY);
294	if (events->inotify_wd < 0) {
295		warn("Couldn't add monitor in dir %s", events->path);
296		return -EIO;
297	}
298
299	printf("Initialized MEMCG events with counters:\n");
300	print_memcg_counters(&events->counters);
301
302	return 0;
303}
304
305static void cleanup_memcg_events(struct memcg_events *events)
306{
307	inotify_rm_watch(events->inotify_fd, events->inotify_wd);
308	close(events->inotify_fd);
309}
310
311int main(int argc, const char **argv)
312{
313	struct memcg_events events;
314	ssize_t ret;
315
316	if (argc != 2)
317		errx(EXIT_FAILURE, "Usage: %s <cgroup>", argv[0]);
318
319	ret = initialize_memcg_events(&events, argv[1]);
320	if (ret)
321		errx(EXIT_FAILURE, "Can't initialize memcg events (%zd)", ret);
322
323	monitor_events(&events);
324
325	cleanup_memcg_events(&events);
326
327	printf("Exiting memcg event listener...\n");
328
329	return EXIT_SUCCESS;
330}
331