#[cfg(target_os = "linux")]
use std::io;
#[cfg(target_os = "linux")]
use std::mem;
#[cfg(target_os = "linux")]
const NETLINK_GENERIC: libc::c_int = 16;
#[cfg(target_os = "linux")]
const GENL_ID_CTRL: u16 = 0x10;
#[cfg(target_os = "linux")]
const CTRL_CMD_GETFAMILY: u8 = 3;
#[cfg(target_os = "linux")]
const CTRL_ATTR_FAMILY_ID: u16 = 1;
#[cfg(target_os = "linux")]
const CTRL_ATTR_FAMILY_NAME: u16 = 2;
#[cfg(target_os = "linux")]
const TASKSTATS_CMD_GET: u8 = 1;
#[cfg(target_os = "linux")]
const TASKSTATS_CMD_ATTR_PID: u16 = 1;
#[cfg(target_os = "linux")]
const TASKSTATS_TYPE_STATS: u16 = 3;
#[cfg(target_os = "linux")]
const TASKSTATS_TYPE_AGGR_PID: u16 = 4;
#[cfg(target_os = "linux")]
const TASKSTATS_TYPE_AGGR_TGID: u16 = 5;
#[cfg(target_os = "linux")]
const NLMSG_ERROR: u16 = 2;
#[cfg(target_os = "linux")]
const NLM_F_REQUEST: u16 = 1;
mod off {
pub const VERSION: usize = 0;
pub const CPU_DELAY_TOTAL: usize = 16;
pub const BLKIO_DELAY_TOTAL: usize = 24;
pub const SWAPIN_DELAY_TOTAL: usize = 32;
pub const AC_ETIME: usize = 72;
}
const MIN_STATS_LEN: usize = off::AC_ETIME + 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DelayStats {
pub cpu_delay_ns: u64,
pub blkio_delay_ns: u64,
pub swapin_delay_ns: u64,
pub elapsed_us: u64,
}
impl DelayStats {
pub fn total_delay_ns(self) -> u64 {
self.cpu_delay_ns
.saturating_add(self.blkio_delay_ns)
.saturating_add(self.swapin_delay_ns)
}
pub fn cpu_percent(self) -> Option<f32> {
self.percent_of(self.cpu_delay_ns)
}
pub fn blkio_percent(self) -> Option<f32> {
self.percent_of(self.blkio_delay_ns)
}
pub fn swapin_percent(self) -> Option<f32> {
self.percent_of(self.swapin_delay_ns)
}
fn percent_of(self, delay_ns: u64) -> Option<f32> {
let life_ns = self.elapsed_us.checked_mul(1000)?;
if life_ns == 0 {
return None;
}
let pct = delay_ns as f64 / life_ns as f64 * 100.0;
Some(pct.clamp(0.0, 100.0) as f32)
}
}
#[cfg(target_os = "linux")]
pub struct DelayAcct {
fd: libc::c_int,
family: u16,
seq: u32,
denied: bool,
}
#[cfg(target_os = "linux")]
impl DelayAcct {
pub fn open() -> Option<Self> {
let fd = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
NETLINK_GENERIC,
)
};
if fd < 0 {
return None;
}
let mut acct = Self {
fd,
family: 0,
seq: 1,
denied: false,
};
let mut addr: libc::sockaddr_nl = unsafe { mem::zeroed() };
addr.nl_family = libc::AF_NETLINK as u16;
let rc = unsafe {
libc::bind(
fd,
&addr as *const libc::sockaddr_nl as *const libc::sockaddr,
mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
)
};
if rc < 0 {
acct.close();
return None;
}
let tv = libc::timeval {
tv_sec: 0,
tv_usec: 200_000,
};
unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_RCVTIMEO,
&tv as *const libc::timeval as *const libc::c_void,
mem::size_of::<libc::timeval>() as libc::socklen_t,
);
}
acct.family = acct.resolve_family()?;
Some(acct)
}
pub fn is_denied(&self) -> bool {
self.denied
}
fn close(&mut self) {
if self.fd >= 0 {
unsafe { libc::close(self.fd) };
self.fd = -1;
}
}
fn resolve_family(&mut self) -> Option<u16> {
let mut msg = Msg::new();
msg.put_genl_header(GENL_ID_CTRL, CTRL_CMD_GETFAMILY);
msg.put_attr_str(CTRL_ATTR_FAMILY_NAME, b"TASKSTATS");
let seq = self.next_seq();
let reply = self.transact(&msg.finish(seq, GENL_ID_CTRL), seq)?;
let attrs = Attrs::new(&reply, NLMSG_HDR_LEN + GENL_HDR_LEN, reply.len());
for (kind, value) in attrs {
if kind == CTRL_ATTR_FAMILY_ID && value.len() >= 2 {
return Some(u16::from_ne_bytes([value[0], value[1]]));
}
}
None
}
pub fn read(&mut self, pid: crate::Pid) -> Option<DelayStats> {
if self.denied || self.fd < 0 || self.family == 0 {
return None;
}
let mut msg = Msg::new();
msg.put_genl_header(self.family, TASKSTATS_CMD_GET);
msg.put_attr_u32(TASKSTATS_CMD_ATTR_PID, pid);
let seq = self.next_seq();
let request = msg.finish(seq, self.family);
let reply = self.transact(&request, seq)?;
let attrs = Attrs::new(&reply, NLMSG_HDR_LEN + GENL_HDR_LEN, reply.len());
for (kind, value) in attrs {
if kind == TASKSTATS_TYPE_AGGR_PID || kind == TASKSTATS_TYPE_AGGR_TGID {
let inner = Attrs::new(value, 0, value.len());
for (inner_kind, inner_value) in inner {
if inner_kind == TASKSTATS_TYPE_STATS {
return parse_taskstats(inner_value);
}
}
} else if kind == TASKSTATS_TYPE_STATS {
return parse_taskstats(value);
}
}
None
}
fn next_seq(&mut self) -> u32 {
self.seq = self.seq.wrapping_add(1);
self.seq
}
fn transact(&mut self, request: &[u8], seq: u32) -> Option<Vec<u8>> {
let sent = unsafe {
libc::send(
self.fd,
request.as_ptr() as *const libc::c_void,
request.len(),
0,
)
};
if sent < 0 {
return None;
}
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = unsafe {
libc::recv(
self.fd,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
0,
)
};
if n < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EAGAIN) {
return None;
}
if err.raw_os_error() == Some(libc::EPERM) {
self.denied = true;
}
return None;
}
let n = n as usize;
buf.truncate(n);
if n < NLMSG_HDR_LEN {
return None;
}
let kind = u16::from_ne_bytes([buf[4], buf[5]]);
let reply_seq = u32::from_ne_bytes([buf[8], buf[9], buf[10], buf[11]]);
if kind == NLMSG_ERROR {
let code = if n >= NLMSG_HDR_LEN + 4 {
i32::from_ne_bytes([buf[16], buf[17], buf[18], buf[19]])
} else {
0
};
if code == -libc::EPERM {
self.denied = true;
}
return None;
}
if reply_seq != seq {
continue;
}
return Some(buf);
}
}
}
#[cfg(target_os = "linux")]
impl Drop for DelayAcct {
fn drop(&mut self) {
self.close();
}
}
pub fn parse_taskstats(buf: &[u8]) -> Option<DelayStats> {
if buf.len() < MIN_STATS_LEN {
return None;
}
let version = u16::from_ne_bytes([buf[off::VERSION], buf[off::VERSION + 1]]);
if version == 0 || version > 64 {
return None;
}
let u64_at = |offset: usize| {
let mut b = [0u8; 8];
b.copy_from_slice(&buf[offset..offset + 8]);
u64::from_ne_bytes(b)
};
Some(DelayStats {
cpu_delay_ns: u64_at(off::CPU_DELAY_TOTAL),
blkio_delay_ns: u64_at(off::BLKIO_DELAY_TOTAL),
swapin_delay_ns: u64_at(off::SWAPIN_DELAY_TOTAL),
elapsed_us: u64_at(off::AC_ETIME),
})
}
#[cfg(target_os = "linux")]
const NLMSG_HDR_LEN: usize = 16;
#[cfg(target_os = "linux")]
const GENL_HDR_LEN: usize = 4;
#[cfg(target_os = "linux")]
struct Msg {
buf: Vec<u8>,
}
#[cfg(target_os = "linux")]
impl Msg {
fn new() -> Self {
Self {
buf: Vec::with_capacity(128),
}
}
fn put_genl_header(&mut self, _family: u16, cmd: u8) {
self.buf.extend_from_slice(&[0u8; NLMSG_HDR_LEN]);
self.buf.extend_from_slice(&[cmd, 1, 0, 0]);
}
fn put_attr(&mut self, kind: u16, value: &[u8]) {
let len = (4 + value.len()) as u16;
self.buf.extend_from_slice(&len.to_ne_bytes());
self.buf.extend_from_slice(&kind.to_ne_bytes());
self.buf.extend_from_slice(value);
while !self.buf.len().is_multiple_of(4) {
self.buf.push(0);
}
}
fn put_attr_str(&mut self, kind: u16, value: &[u8]) {
let mut v = value.to_vec();
v.push(0);
self.put_attr(kind, &v);
}
fn put_attr_u32(&mut self, kind: u16, value: u32) {
self.put_attr(kind, &value.to_ne_bytes());
}
fn finish(mut self, seq: u32, kind: u16) -> Vec<u8> {
let len = self.buf.len() as u32;
self.buf[0..4].copy_from_slice(&len.to_ne_bytes());
self.buf[4..6].copy_from_slice(&kind.to_ne_bytes());
self.buf[6..8].copy_from_slice(&NLM_F_REQUEST.to_ne_bytes());
self.buf[8..12].copy_from_slice(&seq.to_ne_bytes());
self.buf[12..16].copy_from_slice(&0u32.to_ne_bytes());
self.buf
}
}
#[cfg(target_os = "linux")]
struct Attrs<'a> {
buf: &'a [u8],
pos: usize,
end: usize,
}
#[cfg(target_os = "linux")]
impl<'a> Attrs<'a> {
fn new(buf: &'a [u8], start: usize, end: usize) -> Self {
Self {
buf,
pos: start,
end: end.min(buf.len()),
}
}
}
#[cfg(target_os = "linux")]
impl<'a> Iterator for Attrs<'a> {
type Item = (u16, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
if self.pos + 4 > self.end {
return None;
}
let len = u16::from_ne_bytes([self.buf[self.pos], self.buf[self.pos + 1]]) as usize;
let kind = u16::from_ne_bytes([self.buf[self.pos + 2], self.buf[self.pos + 3]]);
if len < 4 || self.pos + len > self.end {
return None;
}
let value = &self.buf[self.pos + 4..self.pos + len];
self.pos += (len + 3) & !3;
Some((kind, value))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stats_blob(version: u16, cpu: u64, blkio: u64, swapin: u64, etime: u64) -> Vec<u8> {
let mut buf = vec![0u8; MIN_STATS_LEN];
buf[off::VERSION..off::VERSION + 2].copy_from_slice(&version.to_ne_bytes());
buf[off::CPU_DELAY_TOTAL..off::CPU_DELAY_TOTAL + 8].copy_from_slice(&cpu.to_ne_bytes());
buf[off::BLKIO_DELAY_TOTAL..off::BLKIO_DELAY_TOTAL + 8]
.copy_from_slice(&blkio.to_ne_bytes());
buf[off::SWAPIN_DELAY_TOTAL..off::SWAPIN_DELAY_TOTAL + 8]
.copy_from_slice(&swapin.to_ne_bytes());
buf[off::AC_ETIME..off::AC_ETIME + 8].copy_from_slice(&etime.to_ne_bytes());
buf
}
#[test]
fn parses_the_four_fields_at_their_kernel_offsets() {
let blob = stats_blob(17, 1_000, 2_000, 3_000, 4_000_000);
let s = parse_taskstats(&blob).expect("valid blob");
assert_eq!(s.cpu_delay_ns, 1_000);
assert_eq!(s.blkio_delay_ns, 2_000);
assert_eq!(s.swapin_delay_ns, 3_000);
assert_eq!(s.elapsed_us, 4_000_000);
assert_eq!(s.total_delay_ns(), 6_000);
}
#[test]
fn a_short_blob_is_rejected_rather_than_read_past() {
for len in [0, 8, 16, MIN_STATS_LEN - 1] {
assert!(parse_taskstats(&vec![0u8; len]).is_none(), "len {len}");
}
}
#[test]
fn a_zero_version_is_not_a_taskstats_reply() {
let blob = stats_blob(0, 1, 1, 1, 1);
assert!(parse_taskstats(&blob).is_none());
}
#[test]
fn percentages_are_delay_over_lifetime() {
let s = DelayStats {
cpu_delay_ns: 50_000_000,
blkio_delay_ns: 10_000_000,
swapin_delay_ns: 0,
elapsed_us: 100_000, };
assert_eq!(s.cpu_percent(), Some(50.0));
assert_eq!(s.blkio_percent(), Some(10.0));
assert_eq!(s.swapin_percent(), Some(0.0));
}
#[test]
fn zero_elapsed_time_has_no_percentage() {
let s = DelayStats {
cpu_delay_ns: 0,
blkio_delay_ns: 0,
swapin_delay_ns: 0,
elapsed_us: 0,
};
assert_eq!(s.cpu_percent(), None);
assert_eq!(s.blkio_percent(), None);
assert_eq!(s.swapin_percent(), None);
}
#[test]
fn delay_above_lifetime_is_clamped_to_100() {
let s = DelayStats {
cpu_delay_ns: 500_000_000,
blkio_delay_ns: 0,
swapin_delay_ns: 0,
elapsed_us: 1_000, };
assert_eq!(s.cpu_percent(), Some(100.0));
}
#[cfg(target_os = "linux")]
#[test]
fn attributes_are_walked_with_padding() {
let mut buf = Vec::new();
buf.extend_from_slice(&8u16.to_ne_bytes());
buf.extend_from_slice(&1u16.to_ne_bytes());
buf.extend_from_slice(&7u32.to_ne_bytes());
let mut msg = Msg::new();
msg.put_genl_header(1, 1);
msg.put_attr(2, b"abcd");
msg.put_attr_str(3, b"xy"); let full = msg.finish(1, 1);
let mut attrs = Attrs::new(&full, NLMSG_HDR_LEN + GENL_HDR_LEN, full.len());
let (kind, value) = attrs.next().expect("first");
assert_eq!(kind, 2);
assert_eq!(value, b"abcd");
let (kind, value) = attrs.next().expect("second");
assert_eq!(kind, 3);
assert_eq!(value, b"xy\0");
assert!(attrs.next().is_none());
assert_eq!(buf.len(), 8);
}
#[cfg(target_os = "linux")]
#[test]
fn a_truncated_attribute_ends_the_walk_instead_of_panicking() {
let mut msg = Msg::new();
msg.put_genl_header(1, 1);
msg.put_attr(2, b"abcd");
let mut full = msg.finish(1, 1);
full.truncate(full.len() - 2); let mut attrs = Attrs::new(&full, NLMSG_HDR_LEN + GENL_HDR_LEN, full.len());
assert!(attrs.next().is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn finish_writes_a_consistent_netlink_header() {
let mut msg = Msg::new();
msg.put_genl_header(0x22, 7);
let dgram = msg.finish(99, 0x22);
let len = u32::from_ne_bytes([dgram[0], dgram[1], dgram[2], dgram[3]]) as usize;
assert_eq!(len, dgram.len());
assert_eq!(u16::from_ne_bytes([dgram[4], dgram[5]]), 0x22);
assert_eq!(u16::from_ne_bytes([dgram[6], dgram[7]]), NLM_F_REQUEST);
assert_eq!(
u32::from_ne_bytes([dgram[8], dgram[9], dgram[10], dgram[11]]),
99
);
assert_eq!(dgram[NLMSG_HDR_LEN], 7, "genl command");
}
}