| // SPDX-License-Identifier: GPL-2.0 |
| #define _GNU_SOURCE |
| |
| #include <errno.h> |
| #include <linux/genetlink.h> |
| #include <linux/netlink.h> |
| #include <linux/taskstats.h> |
| #include <pthread.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| #include <time.h> |
| #include <unistd.h> |
| |
| #include "kselftest.h" |
| |
| #ifndef NLA_ALIGN |
| #define NLA_ALIGNTO 4 |
| #define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1)) |
| #define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr))) |
| #endif |
| |
| #define BUSY_NS (200ULL * 1000 * 1000) |
| |
| struct worker_ctx { |
| pthread_mutex_t lock; |
| pthread_cond_t cond; |
| bool ready; |
| bool release; |
| }; |
| |
| static unsigned long busy_sink; |
| |
| static void *taskstats_nla_data(const struct nlattr *na) |
| { |
| return (void *)((char *)na + NLA_HDRLEN); |
| } |
| |
| static bool taskstats_nla_ok(const struct nlattr *na, int remaining) |
| { |
| return remaining >= (int)sizeof(*na) && |
| na->nla_len >= sizeof(*na) && |
| na->nla_len <= remaining; |
| } |
| |
| static struct nlattr *taskstats_nla_next(const struct nlattr *na, int *remaining) |
| { |
| int aligned_len = NLA_ALIGN(na->nla_len); |
| |
| *remaining -= aligned_len; |
| return (struct nlattr *)((char *)na + aligned_len); |
| } |
| |
| static uint64_t timespec_diff_ns(const struct timespec *start, |
| const struct timespec *end) |
| { |
| return (uint64_t)(end->tv_sec - start->tv_sec) * 1000000000ULL + |
| (uint64_t)(end->tv_nsec - start->tv_nsec); |
| } |
| |
| static void burn_cpu_for_ns(uint64_t runtime_ns) |
| { |
| struct timespec start, now; |
| unsigned long acc = 0; |
| |
| if (clock_gettime(CLOCK_MONOTONIC, &start)) { |
| perror("clock_gettime"); |
| exit(EXIT_FAILURE); |
| } |
| |
| do { |
| for (int i = 0; i < 100000; i++) |
| acc += i; |
| if (clock_gettime(CLOCK_MONOTONIC, &now)) { |
| perror("clock_gettime"); |
| exit(EXIT_FAILURE); |
| } |
| } while (timespec_diff_ns(&start, &now) < runtime_ns); |
| |
| busy_sink = acc; |
| } |
| |
| static int netlink_open(void) |
| { |
| struct sockaddr_nl addr = { |
| .nl_family = AF_NETLINK, |
| .nl_pid = getpid(), |
| }; |
| int fd; |
| |
| fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); |
| if (fd < 0) |
| return -errno; |
| |
| if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { |
| int err = -errno; |
| |
| close(fd); |
| return err; |
| } |
| |
| return fd; |
| } |
| |
| static int send_request(int fd, void *buf, size_t len) |
| { |
| struct sockaddr_nl addr = { |
| .nl_family = AF_NETLINK, |
| }; |
| |
| if (sendto(fd, buf, len, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0) |
| return -errno; |
| |
| return 0; |
| } |
| |
| static int get_family_id(int fd, const char *name) |
| { |
| struct { |
| struct nlmsghdr nlh; |
| struct genlmsghdr genl; |
| char buf[256]; |
| } req = { 0 }; |
| char resp[8192]; |
| struct nlmsghdr *nlh; |
| struct genlmsghdr *genl; |
| struct nlattr *na; |
| int len; |
| int rem; |
| int ret; |
| |
| req.nlh.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN); |
| req.nlh.nlmsg_type = GENL_ID_CTRL; |
| req.nlh.nlmsg_flags = NLM_F_REQUEST; |
| req.nlh.nlmsg_seq = 1; |
| req.nlh.nlmsg_pid = getpid(); |
| |
| req.genl.cmd = CTRL_CMD_GETFAMILY; |
| req.genl.version = 1; |
| |
| na = (struct nlattr *)((char *)&req + NLMSG_ALIGN(req.nlh.nlmsg_len)); |
| na->nla_type = CTRL_ATTR_FAMILY_NAME; |
| na->nla_len = NLA_HDRLEN + strlen(name) + 1; |
| memcpy(taskstats_nla_data(na), name, strlen(name) + 1); |
| req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + NLA_ALIGN(na->nla_len); |
| |
| ret = send_request(fd, &req, req.nlh.nlmsg_len); |
| if (ret) |
| return ret; |
| |
| len = recv(fd, resp, sizeof(resp), 0); |
| if (len < 0) |
| return -errno; |
| |
| for (nlh = (struct nlmsghdr *)resp; NLMSG_OK(nlh, len); |
| nlh = NLMSG_NEXT(nlh, len)) { |
| if (nlh->nlmsg_type == NLMSG_ERROR) { |
| struct nlmsgerr *err = NLMSG_DATA(nlh); |
| |
| return err->error ? err->error : -ENOENT; |
| } |
| |
| genl = (struct genlmsghdr *)NLMSG_DATA(nlh); |
| rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; |
| na = (struct nlattr *)((char *)genl + GENL_HDRLEN); |
| while (taskstats_nla_ok(na, rem)) { |
| if (na->nla_type == CTRL_ATTR_FAMILY_ID) |
| return *(uint16_t *)taskstats_nla_data(na); |
| na = taskstats_nla_next(na, &rem); |
| } |
| } |
| |
| return -ENOENT; |
| } |
| |
| static int get_taskstats(int fd, int family_id, uint16_t attr_type, uint32_t id, |
| struct taskstats *stats) |
| { |
| struct { |
| struct nlmsghdr nlh; |
| struct genlmsghdr genl; |
| char buf[256]; |
| } req = { 0 }; |
| char resp[16384]; |
| struct nlmsghdr *nlh; |
| struct genlmsghdr *genl; |
| struct nlattr *na; |
| struct nlattr *nested; |
| int len; |
| int rem; |
| int nrem; |
| int ret; |
| |
| memset(stats, 0, sizeof(*stats)); |
| |
| req.nlh.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN); |
| req.nlh.nlmsg_type = family_id; |
| req.nlh.nlmsg_flags = NLM_F_REQUEST; |
| req.nlh.nlmsg_seq = 2; |
| req.nlh.nlmsg_pid = getpid(); |
| |
| req.genl.cmd = TASKSTATS_CMD_GET; |
| req.genl.version = 1; |
| |
| na = (struct nlattr *)((char *)&req + NLMSG_ALIGN(req.nlh.nlmsg_len)); |
| na->nla_type = attr_type; |
| na->nla_len = NLA_HDRLEN + sizeof(id); |
| memcpy(taskstats_nla_data(na), &id, sizeof(id)); |
| req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + NLA_ALIGN(na->nla_len); |
| |
| ret = send_request(fd, &req, req.nlh.nlmsg_len); |
| if (ret) |
| return ret; |
| |
| len = recv(fd, resp, sizeof(resp), 0); |
| if (len < 0) |
| return -errno; |
| |
| for (nlh = (struct nlmsghdr *)resp; NLMSG_OK(nlh, len); |
| nlh = NLMSG_NEXT(nlh, len)) { |
| if (nlh->nlmsg_type == NLMSG_ERROR) { |
| struct nlmsgerr *err = NLMSG_DATA(nlh); |
| |
| return err->error ? err->error : -ENOENT; |
| } |
| |
| genl = (struct genlmsghdr *)NLMSG_DATA(nlh); |
| rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; |
| na = (struct nlattr *)((char *)genl + GENL_HDRLEN); |
| while (taskstats_nla_ok(na, rem)) { |
| if (na->nla_type == TASKSTATS_TYPE_AGGR_PID || |
| na->nla_type == TASKSTATS_TYPE_AGGR_TGID) { |
| nested = (struct nlattr *)taskstats_nla_data(na); |
| nrem = na->nla_len - NLA_HDRLEN; |
| while (taskstats_nla_ok(nested, nrem)) { |
| if (nested->nla_type == TASKSTATS_TYPE_STATS) { |
| memcpy(stats, taskstats_nla_data(nested), |
| sizeof(*stats)); |
| return 0; |
| } |
| nested = taskstats_nla_next(nested, &nrem); |
| } |
| } |
| na = taskstats_nla_next(na, &rem); |
| } |
| } |
| |
| return -ENOENT; |
| } |
| |
| static uint64_t cpu_total(const struct taskstats *stats) |
| { |
| return (uint64_t)stats->ac_utime + (uint64_t)stats->ac_stime; |
| } |
| |
| static void print_stats(const char *label, const struct taskstats *stats) |
| { |
| ksft_print_msg("%s: cpu_total=%llu nvcsw=%llu nivcsw=%llu\n", |
| label, (unsigned long long)cpu_total(stats), |
| (unsigned long long)stats->nvcsw, |
| (unsigned long long)stats->nivcsw); |
| } |
| |
| static void *worker_thread(void *arg) |
| { |
| struct worker_ctx *ctx = arg; |
| |
| burn_cpu_for_ns(BUSY_NS); |
| |
| pthread_mutex_lock(&ctx->lock); |
| ctx->ready = true; |
| pthread_cond_broadcast(&ctx->cond); |
| while (!ctx->release) |
| pthread_cond_wait(&ctx->cond, &ctx->lock); |
| pthread_mutex_unlock(&ctx->lock); |
| |
| return NULL; |
| } |
| |
| int main(void) |
| { |
| struct worker_ctx ctx = { |
| .lock = PTHREAD_MUTEX_INITIALIZER, |
| .cond = PTHREAD_COND_INITIALIZER, |
| }; |
| struct taskstats before, after; |
| pthread_t thread; |
| pid_t tgid = getpid(); |
| int family_id; |
| int fd; |
| int ret; |
| |
| ksft_print_header(); |
| ksft_set_plan(1); |
| |
| if (geteuid()) |
| ksft_exit_skip("taskstats_fill_stats_tgid needs root\n"); |
| |
| fd = netlink_open(); |
| if (fd < 0) |
| ksft_exit_skip("failed to open generic netlink socket: %s\n", |
| strerror(-fd)); |
| |
| family_id = get_family_id(fd, TASKSTATS_GENL_NAME); |
| if (family_id < 0) |
| ksft_exit_skip("taskstats generic netlink family unavailable: %s\n", |
| strerror(-family_id)); |
| |
| /* Create worker thread that burns 200ms of CPU */ |
| if (pthread_create(&thread, NULL, worker_thread, &ctx) != 0) |
| ksft_exit_fail_msg("pthread_create failed: %s\n", strerror(errno)); |
| |
| /* Wait for worker to finish generating activity */ |
| pthread_mutex_lock(&ctx.lock); |
| while (!ctx.ready) |
| pthread_cond_wait(&ctx.cond, &ctx.lock); |
| pthread_mutex_unlock(&ctx.lock); |
| |
| /* |
| * Snapshot A: TGID stats while worker is alive and sleeping. |
| * Contains main thread + worker contributions. |
| */ |
| ret = get_taskstats(fd, family_id, TASKSTATS_CMD_ATTR_TGID, tgid, &before); |
| if (ret) |
| ksft_exit_fail_msg("TGID query before exit failed: %s\n", |
| strerror(-ret)); |
| |
| /* Release worker so it can exit, then join (deterministic wait). |
| * |
| * Kernel exit path ordering guarantees: |
| * do_exit() |
| * taskstats_exit() -> fill_tgid_exit() (accumulates worker into signal->stats) |
| * exit_notify() (releases the thread) |
| * do_task_dead() -> __schedule() (wakes joiner) |
| * |
| * So pthread_join() returns only after fill_tgid_exit() has completed. |
| */ |
| pthread_mutex_lock(&ctx.lock); |
| ctx.release = true; |
| pthread_cond_broadcast(&ctx.cond); |
| pthread_mutex_unlock(&ctx.lock); |
| |
| pthread_join(thread, NULL); |
| |
| /* |
| * Snapshot B: TGID stats after worker has exited. |
| * fill_stats_for_tgid() does: |
| * memcpy(signal->stats) <- includes fill_tgid_exit accumulation |
| * + scan live threads <- only main thread now |
| */ |
| ret = get_taskstats(fd, family_id, TASKSTATS_CMD_ATTR_TGID, tgid, &after); |
| if (ret) |
| ksft_exit_fail_msg("TGID query after exit failed: %s\n", |
| strerror(-ret)); |
| |
| print_stats("TGID before worker exit", &before); |
| print_stats("TGID after worker exit", &after); |
| |
| /* |
| * The worker burned 200ms of CPU before the first snapshot. |
| * If the kernel correctly retained its contribution via |
| * fill_tgid_exit(), then the TGID CPU total after exit must be at |
| * least as large as the TGID CPU total before exit. |
| */ |
| ksft_test_result(cpu_total(&after) >= cpu_total(&before), |
| "TGID CPU stats should not regress after thread exit\n"); |
| |
| close(fd); |
| ksft_finished(); |
| return ksft_get_fail_cnt() ? KSFT_FAIL : KSFT_PASS; |
| } |