376 lines
9.3 KiB
C
376 lines
9.3 KiB
C
// 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;
|
|
}
|