Skip to content

Add per process network I/O rate meters - #2105

Draft
fasterit wants to merge 11 commits into
htop-dev:mainfrom
fasterit:add-per-process-network-meters
Draft

fasterit wants to merge 11 commits into
htop-dev:mainfrom
fasterit:add-per-process-network-meters

Conversation

@fasterit

Copy link
Copy Markdown
Member

These come in three flavors:

Exact: eBPF Kprobes; these need libbpf, root or CAP_BPF/CAP_SYS_ADMIN
at runtime. And clang with assorted horsetoppings at build time.
This measures TCP, UDP and ICMP both IPv4 and IPv6.
--enable-ebpf-net, no prefix in the columns.

Concise: NETLINK_SOCK_DIAG based; this needs libnl-3 (might the there from
delayacct already). This measures TCP and gets a lower bound from
socket-buffer queue levels for UDP and ICMP).
--enable-libnl-net, "+" in the columns.

Estimate: This uses read/write byte counters and subtracts disk I/O, so
it is crap when pipes are being used, too.
Always enabled, "~" in the columns.

This is a massive feature, so it needs thorough testing and review.
There is helpful diagnostic printing on stderr when --enable-debug is configured. We may want to move that to a separate IFDEF or drop before merging this feature. At least no other features babble on stderr to make our lives easier.

This is a Linux-only feature.
Developed and tested on Debian, so testing on other distros and kernels is most appreciated.

@fasterit fasterit added enhancement Extension or improvement to existing feature Linux 🐧 Linux related issues labels Sep 13, 2026
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4eed45a7-52d5-4328-90d7-ef75bf9e26be

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4d0f6 and 7a1cd14.

📒 Files selected for processing (1)
  • linux/LinuxProcessTable.c

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The change adds optional Linux network monitoring through eBPF and NETLINK_SOCK_DIAG. It adds build-time detection, embedded eBPF compilation, dynamic runtime loading, capability handling, and cleanup. It adds receive, transmit, and total per-process network columns. Rate calculation uses eBPF, netlink, or bounded rchar/wchar estimates. Documentation describes configuration, dependencies, measurement sources, and limitations.

Suggested reviewers: benbe

Priority: ➖ Normal

Change: Feature

Merge Risk: 🟡 Moderate · up to 7a1cd

Network refreshes can stall on large hosts, and failed eBPF cleanup can leave load-time privileges active. These risks should be addressed before merging.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Packets trace through kernel streams
Counters wake from quiet dreams
eBPF leads, netlink follows
Fallback keeps the columns glowing
RX and TX now mark the way

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)
linux/LinuxProcessTable.c (1)

641-800: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle realtime clock rollback in NetRateWindow_update.

LinuxProcessTable_readIoFile and LinuxProcessTable_readNetIO are active scan paths at lines 1893-1897. They pass host->realtimeMs to NetRateWindow_update, while ScreenManager.c explicitly detects realtime clock adjustments. saturatingSub prevents unsigned wraparound, but a backward step leaves window_start_ms in the future. The window can retain stale rates, then calculate an inflated rate when the clock reaches the old window start because the denominator excludes the rollback interval. Use host->monotonicMs for these rate-window timestamps, or reseed when now < w->window_start_ms.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: addac728-0105-4ccc-809f-fa80a484b3a0

📥 Commits

Reviewing files that changed from the base of the PR and between 07fbb8d and 914a809.

📒 Files selected for processing (20)
  • Makefile.am
  • README.md
  • Row.c
  • Row.h
  • XUtils.c
  • XUtils.h
  • configure.ac
  • htop.1.in
  • linux/LinuxProcess.c
  • linux/LinuxProcess.h
  • linux/LinuxProcessTable.c
  • linux/NetLinkNet.c
  • linux/NetLinkNet.h
  • linux/NetMonitor.bpf.c
  • linux/NetMonitor.c
  • linux/NetMonitor.h
  • linux/Platform.c
  • linux/ProcessField.h
  • pcp/PCPDynamicColumn.c
  • pcp/PCPProcess.c

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread configure.ac Outdated
Comment thread htop.1.in Outdated
Comment thread htop.1.in Outdated
Comment thread linux/LinuxProcessTable.c Outdated
Comment thread linux/NetLinkNet.c
Comment on lines +271 to +309
static void NetLinkNet_rescanInodes(void) {
DIR* procDir = opendir(PROCDIR);
if (!procDir)
return;

Hashtable* freshInodePid = Hashtable_new(1024, false);
pidsPresent = Hashtable_new(256, false);

struct dirent* de;
while ((de = readdir(procDir)) != NULL) {
pid_t pid = (pid_t) strtol(de->d_name, NULL, 10);
if (pid <= 0)
continue;

unsigned int tgid = NetLinkNet_readTgid(pid);
if (!tgid)
continue;

if (!Hashtable_get(pidsPresent, tgid))
Hashtable_put(pidsPresent, tgid, (void*) (uintptr_t) tgid);

char fdPath[64];
xSnprintf(fdPath, sizeof(fdPath), PROCDIR "/%s/fd", de->d_name);
DIR* fdDir = opendir(fdPath);
if (!fdDir)
continue;

int fdDirFd = dirfd(fdDir);
struct dirent* fde;
while ((fde = readdir(fdDir)) != NULL) {
struct stat st;
if (fstatat(fdDirFd, fde->d_name, &st, 0) != 0)
continue;
if (S_ISSOCK(st.st_mode))
Hashtable_put(freshInodePid, (ht_key_t) st.st_ino, (void*) (uintptr_t) tgid);
}
closedir(fdDir);
}
closedir(procDir);

@coderabbitai coderabbitai Bot Sep 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Bound the synchronous inode rescan.

ProcessTable_goThroughEntries() calls NetLinkNet_update() in the per-sample path. Every 5000 ms, NetLinkNet_update() synchronously calls NetLinkNet_rescanInodes(). The rescan calls fstatat() for every /proc/<pid>/fd entry, so its work scales with total open file descriptors and can delay refresh completion on high-FD hosts.

Preserve the complete inode-to-TGID map when reducing this work. The /proc/net/{tcp,tcp6,udp,udp6} dumps do not provide the owning TGID, and inodePid is required to attribute socket counters. readlinkat() still performs one operation per descriptor. An unchanged st_nlink also does not prove that descriptor ownership is unchanged. Consider moving the full rescan off the sample thread or implementing a correctness-preserving incremental scan. Raising NETLINK_INODE_RESCAN_MS only reduces frequency and increases mapping staleness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe try if we can skip the fstatat if we know the fd is not a network socket or alike?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline linux/NetLinkNet.c --items all --match 'NetLinkNet_rescanInodes|NetLinkNet_update|NetLinkNet_getNetBytes'
sed -n '219,380p' linux/NetLinkNet.c
rg -n -C 2 'fstatat\(|readlinkat\(|d_type|DT_' linux README.md

Length of output: 9261


@BenBE, not before inspecting the descriptor target.

Each /proc/<pid>/fd/<n> entry is a symlink. readdir() metadata identifies the entry as a symlink, not whether its target is a socket. fstatat(..., 0) follows that link and supplies the target type and inode.

readlinkat() can detect socket:[<inode>], but it still performs one operation for each descriptor. It replaces fstatat(); it does not bound the scan.

A socket-inode set from the NETLINK_SOCK_DIAG dump can avoid unnecessary hash-table work after inspection. It cannot safely skip inspection because descriptor ownership can change between rescans. The original concern therefore remains: a correctness-preserving optimization needs to move or incrementally schedule the full descriptor walk, rather than prefilter entries by type.

You are interacting with an AI system.

Comment thread linux/NetMonitor.c
Comment thread linux/NetMonitor.c Outdated
Comment on lines +281 to +325
void* links[NETMONITOR_MAX_PROGRAMS];
size_t nLinks = 0;
#if !defined(NDEBUG)
struct NetMonitorAttachResult {
const char* section;
bool attached;
int error;
} results[NETMONITOR_MAX_PROGRAMS];
size_t nResults = 0;
bool anyNotAllowed = false;
#endif
void* prog = NULL;
while ((prog = sym_bpf_object__next_program(obj, prog)) != NULL) {
void* link = sym_bpf_program__attach(prog);
#if !defined(NDEBUG)
int error = link ? 0 : errno;
if (nResults < NETMONITOR_MAX_PROGRAMS) {
results[nResults].section = sym_bpf_program__section_name(prog);
results[nResults].attached = link != NULL;
results[nResults].error = error;
nResults++;
}
#endif
if (!link) {
#if !defined(NDEBUG)
if (error == EPERM)
anyNotAllowed = true;
#endif
} else if (nLinks < NETMONITOR_MAX_PROGRAMS) {
links[nLinks++] = link;
}
}
if (!nLinks) {
for (size_t i = 0; i < nLinks; i++)
sym_bpf_link__destroy(links[i]);
goto fail;
}
#if !defined(NDEBUG)
NetMonitor_debug("eBPF probe attach summary (load succeeded):\n");
for (size_t i = 0; i < nResults; i++) {
const char* section = results[i].section ? results[i].section : "<unknown>";
if (results[i].attached) {
NetMonitor_debug(" %-26s attached\n", section);
} else if (results[i].error == ENOENT) {
const char* note = NetMonitor_expectedMissing(section);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require complete core probe coverage before enabling exact mode.

NetMonitor_loadBPF() sets eBPFActive when any probe attaches. It does not require the TCP and UDP send/receive probes or ping_recvmsg. If one core probe fails and another attaches, LinuxProcessTable_readNetIO() still selects the eBPF counters, while LinuxProcess_netMarker() returns no marker. The affected direction or protocol is then omitted from the displayed rate without indicating degraded support.

Require the TCP and UDP direction probes and ping_recvmsg before setting eBPFActive. Keep only the documented expected-missing exceptions for ping_v4_sendmsg and ping_v6_sendmsg. If a required probe is missing, destroy attached links and use the fallback backend.

Comment thread linux/Platform.c Outdated
Comment thread Makefile.am
Comment thread README.md Outdated
@BenBE BenBE added feature request Completely new feature requested and removed enhancement Extension or improvement to existing feature labels Sep 13, 2026
fasterit pushed a commit to fasterit/htop that referenced this pull request Sep 13, 2026
fasterit pushed a commit to fasterit/htop that referenced this pull request Sep 13, 2026
@fasterit
fasterit force-pushed the add-per-process-network-meters branch from 0a4d0f6 to 7a1cd14 Compare September 13, 2026 10:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9906f8ca-4c3e-4866-bc2b-3e35932fd4ba

📥 Commits

Reviewing files that changed from the base of the PR and between eb7bb00 and 0a4d0f6.

📒 Files selected for processing (8)
  • Makefile.am
  • README.md
  • configure.ac
  • htop.1.in
  • linux/LinuxProcessTable.c
  • linux/NetMonitor.c
  • linux/Platform.c
  • linux/Platform.h

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread htop.1.in
Comment thread linux/NetMonitor.c
Comment thread linux/Platform.c Outdated
fasterit pushed a commit to fasterit/htop that referenced this pull request Sep 13, 2026
@fasterit
fasterit force-pushed the add-per-process-network-meters branch 2 times, most recently from 00a5b79 to b53703e Compare September 15, 2026 10:32
Comment thread linux/LinuxProcess.c
Comment on lines +85 to +87
[NET_RX] = { .name = "NET_RX", .title = " NET RX ", .description = "Network receive rate in bytes per second (eBPF exact; + netlink socket counters; ~ read(2)/write(2) estimate)", .flags = PROCESS_FLAG_IO | PROCESS_FLAG_LINUX_NETIO, .defaultSortDesc = true, },
[NET_TX] = { .name = "NET_TX", .title = " NET TX ", .description = "Network transmit rate in bytes per second (eBPF exact; + netlink socket counters; ~ read(2)/write(2) estimate)", .flags = PROCESS_FLAG_IO | PROCESS_FLAG_LINUX_NETIO, .defaultSortDesc = true, },
[NET_TOTAL] = { .name = "NET_TOTAL", .title = " NET TOTAL ", .description = "Total network rate in bytes per second (eBPF exact; + netlink socket counters; ~ read(2)/write(2) estimate)", .flags = PROCESS_FLAG_IO | PROCESS_FLAG_LINUX_NETIO, .defaultSortDesc = true, },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe have them with define guards like delay accounting below? So only when wither netlink or ebpf collection is enabled in configure?

Comment thread linux/LinuxProcessTable.c
Comment on lines +769 to +774
/* A cumulative counter that went backwards means the tracking state was
* reset elsewhere (dropped map entry, PID reuse, readahead counted in
* read_bytes before the corresponding rchar). The previous baseline no
* longer applies, so re-seed instead of trusting a bogus delta that would
* surface as a huge rate. */
if (curRx < w->last_rx_bytes || curTx < w->last_tx_bytes) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this properly account for wrap-around?

Comment thread linux/NetLinkNet.c

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how little we need from libnl for the actual socket setup, a straight up socket-only implementation (without libnl) would be even shorter (one-shot with Copilot):

#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <linux/inet_diag.h>
#include <linux/netlink.h>
#include <linux/sock_diag.h>
#include <linux/tcp.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "Hashtable.h"
#include "Macros.h"
#include "XUtils.h"
#include "linux/LinuxMachine.h"
#include "linux/Platform.h"

#define NETLINK_INODE_RESCAN_MS 5000
#define NETLINK_RECV_BUFSIZE (256 * 1024)

#ifndef TCP_LISTEN
#define TCP_LISTEN 10
#endif

#define NLA_ALIGNTO 4
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr)))

#define NLA_DATA(nla) \
   ((void*)((char*)(nla) + NLA_HDRLEN))

#define NLA_LEN(nla) \
   ((int)((nla)->nla_len - NLA_HDRLEN))

#define NLA_OK(nla, len) \
   ((len) >= (int)sizeof(struct nlattr) && \
    (nla)->nla_len >= sizeof(struct nlattr) && \
    (nla)->nla_len <= (unsigned)(len))

#define NLA_NEXT(nla, len) \
   ((len) -= NLA_ALIGN((nla)->nla_len), \
    (struct nlattr*)(void*)((char*)(nla) + \
    NLA_ALIGN((nla)->nla_len)))

typedef struct SocketState_ {
   unsigned long long lastRx;
   unsigned long long lastTx;
   unsigned int round;
} SocketState;

typedef struct PidTotals_ {
   unsigned long long rx;
   unsigned long long tx;
} PidTotals;

static int diagFd = -1;
static uint32_t nlSeq = 0;

static Hashtable* inodePid = NULL;
static Hashtable* socketState = NULL;
static Hashtable* pidTotals = NULL;
static Hashtable* pidsPresent = NULL;
static Hashtable* unreadablePids = NULL;

static unsigned int roundCounter = 0;
static uint64_t lastRescanMs = 0;
static int gDumpProto = 0;

static void NetLinkNet_debug(const char* fmt, ...) ATTR_FORMAT(printf, 1, 2);

static void NetLinkNet_debug(const char* fmt, ...) {
#ifdef NDEBUG
   (void)fmt;
#else
   va_list ap;
   va_start(ap, fmt);
   fprintf(stderr, "htop-netlink: ");
   vfprintf(stderr, fmt, ap);
   va_end(ap);
#endif
}

static inline unsigned long long
saturatingAdd(unsigned long long a,
              unsigned long long b) {
   return a > ULLONG_MAX - b ? ULLONG_MAX : a + b;
}

/*
 * Keep the inode scanning code from the existing file
 * unchanged.
 *
 * NetLinkNet_readTgid()
 * NetLinkNet_rescanInodes()
 * NetLinkNet_collectStaleSocket()
 * NetLinkNet_dropStaleSocket()
 * NetLinkNet_pruneStaleSockets()
 * NetLinkNet_collectStalePid()
 * NetLinkNet_dropStalePid()
 */

/*****************************************************************************/

static void NetLinkNet_closeSocket(void) {
   if (diagFd >= 0)
      close(diagFd);

   diagFd = -1;
}

static bool NetLinkNet_open(void) {
   diagFd =
      socket(AF_NETLINK,
             SOCK_RAW | SOCK_CLOEXEC,
             NETLINK_SOCK_DIAG);

   if (diagFd < 0)
      return false;

   int rcvbuf = NETLINK_RECV_BUFSIZE;

   (void)setsockopt(diagFd,
                    SOL_SOCKET,
                    SO_RCVBUF,
                    &rcvbuf,
                    sizeof(rcvbuf));

   struct sockaddr_nl local;
   memset(&local, 0, sizeof(local));

   local.nl_family = AF_NETLINK;

   if (bind(diagFd,
            (struct sockaddr*)&local,
            sizeof(local)) < 0) {

      NetLinkNet_closeSocket();
      return false;
   }

   NetLinkNet_debug(
      "netlink network monitoring active\n");

   return true;
}

/*****************************************************************************/

static void NetLinkNet_accumulate(unsigned int inode,
                                  unsigned long long rx,
                                  unsigned long long tx) {
   SocketState* state =
      Hashtable_get(socketState, inode);

   if (!state) {
      state = xCalloc(1, sizeof(*state));

      state->lastRx = rx;
      state->lastTx = tx;
      state->round = roundCounter;

      Hashtable_put(socketState, inode, state);
      return;
   }

   state->round = roundCounter;

   unsigned long long rxDelta =
      saturatingSub(rx, state->lastRx);

   unsigned long long txDelta =
      saturatingSub(tx, state->lastTx);

   state->lastRx = rx;
   state->lastTx = tx;

   if (!rxDelta && !txDelta)
      return;

   unsigned int pid =
      (unsigned int)(uintptr_t)
      Hashtable_get(inodePid, inode);

   if (!pid)
      return;

   PidTotals* totals =
      Hashtable_get(pidTotals, pid);

   if (!totals) {
      totals = xCalloc(1, sizeof(*totals));
      Hashtable_put(pidTotals, pid, totals);
   }

   totals->rx =
      saturatingAdd(totals->rx, rxDelta);

   totals->tx =
      saturatingAdd(totals->tx, txDelta);
}

/*****************************************************************************/

static void NetLinkNet_processMsg(struct nlmsghdr* nlh) {
   if (nlh->nlmsg_len <
       NLMSG_LENGTH(sizeof(struct inet_diag_msg)))
      return;

   struct inet_diag_msg* dm = NLMSG_DATA(nlh);

   if (!dm->idiag_inode)
      return;

   if (gDumpProto == IPPROTO_TCP &&
       dm->idiag_state == TCP_LISTEN)
      return;

   unsigned long long rx = 0;
   unsigned long long tx = 0;

   int remaining =
      (int)(nlh->nlmsg_len -
      NLMSG_LENGTH(sizeof(*dm)));

   struct nlattr* attr =
      (struct nlattr*)(void*)(dm + 1);

   for (; NLA_OK(attr, remaining);
        attr = NLA_NEXT(attr, remaining)) {

      int type =
         attr->nla_type & 0x3fff;

      void* data = NLA_DATA(attr);
      int len = NLA_LEN(attr);

      if (gDumpProto == IPPROTO_TCP &&
          type == INET_DIAG_INFO) {

         int offsetRx =
            (int)offsetof(
               struct tcp_info,
               tcpi_bytes_received);

         int offsetTx =
            (int)offsetof(
               struct tcp_info,
               tcpi_bytes_acked);

         if (len >= offsetRx + 8)
            memcpy(&rx,
                   (char*)data + offsetRx,
                   sizeof(rx));

         if (len >= offsetTx + 8)
            memcpy(&tx,
                   (char*)data + offsetTx,
                   sizeof(tx));

      } else if (gDumpProto != IPPROTO_TCP &&
                 type == INET_DIAG_MEMINFO) {

         uint32_t* mem = data;

         if (len >= (int)(5 * sizeof(uint32_t))) {
            rx = mem[0];
            tx = mem[3];
         } else if (len >= (int)(2 * sizeof(uint32_t))) {
            rx = mem[0];
            tx = mem[1];
         }
      }
   }

   NetLinkNet_accumulate(dm->idiag_inode,
                         rx,
                         tx);
}

/*****************************************************************************/

static int NetLinkNet_dumpOnce(int family,
                               int proto) {
   struct {
      struct nlmsghdr nlh;
      struct inet_diag_req_v2 req;
   } request;

   memset(&request, 0, sizeof(request));

   request.nlh.nlmsg_len =
      NLMSG_LENGTH(sizeof(request.req));

   request.nlh.nlmsg_type =
      SOCK_DIAG_BY_FAMILY;

   request.nlh.nlmsg_flags =
      NLM_F_REQUEST |
      NLM_F_DUMP;

   request.nlh.nlmsg_seq =
      ++nlSeq;

   request.req.sdiag_family =
      (unsigned char)family;

   request.req.sdiag_protocol =
      (unsigned char)proto;

   request.req.idiag_ext =
      (1 << (INET_DIAG_MEMINFO - 1)) |
      (1 << (INET_DIAG_INFO - 1));

   request.req.idiag_states = ~0U;

   gDumpProto = proto;

   if (send(diagFd,
            &request,
            sizeof(request),
            0) < 0)
      return -1;

   for (;;) {

      char buffer[NETLINK_RECV_BUFSIZE];

      struct sockaddr_nl nladdr;
      struct iovec iov = {
         .iov_base = buffer,
         .iov_len = sizeof(buffer),
      };

      struct msghdr msg;

      memset(&msg, 0, sizeof(msg));

      msg.msg_name = &nladdr;
      msg.msg_namelen = sizeof(nladdr);
      msg.msg_iov = &iov;
      msg.msg_iovlen = 1;

      ssize_t len =
         recvmsg(diagFd, &msg, 0);

      if (len < 0)
         return -1;

      if (nladdr.nl_pid != 0)
         continue;

      struct nlmsghdr* nlh;

      for (nlh = (struct nlmsghdr*)buffer;
           NLMSG_OK(nlh, len);
           nlh = NLMSG_NEXT(nlh, len)) {

         if (nlh->nlmsg_seq != nlSeq)
            continue;

         switch (nlh->nlmsg_type) {

         case NLMSG_DONE:
            return 0;

         case NLMSG_ERROR: {
            struct nlmsgerr* err =
               NLMSG_DATA(nlh);

            if (err->error)
               return -1;

            break;
         }

         default:
            NetLinkNet_processMsg(nlh);
            break;
         }
      }
   }
}

/*****************************************************************************/

void NetLinkNet_init(void) {
   diagFd = -1;
   nlSeq = 0;

   inodePid = NULL;
   socketState = NULL;
   pidTotals = NULL;
   pidsPresent = NULL;
   unreadablePids = NULL;

   roundCounter = 0;
   lastRescanMs = 0;
}

void NetLinkNet_done(void) {
   NetLinkNet_closeSocket();

   if (inodePid)
      Hashtable_delete(inodePid);

   if (socketState)
      Hashtable_delete(socketState);

   if (pidTotals)
      Hashtable_delete(pidTotals);

   if (pidsPresent)
      Hashtable_delete(pidsPresent);

   if (unreadablePids)
      Hashtable_delete(unreadablePids);

   inodePid = NULL;
   socketState = NULL;
   pidTotals = NULL;
   pidsPresent = NULL;
   unreadablePids = NULL;
}

bool NetLinkNet_isActive(void) {
   return diagFd >= 0;
}

void NetLinkNet_update(void) {
   if (diagFd < 0 && !NetLinkNet_open())
      return;

   if (!socketState)
      socketState = Hashtable_new(512, true);

   if (!pidTotals)
      pidTotals = Hashtable_new(128, true);

   uint64_t nowMs;

   Platform_gettime_monotonic(&nowMs);

   if (!inodePid ||
       nowMs - lastRescanMs >=
       NETLINK_INODE_RESCAN_MS) {

      NetLinkNet_rescanInodes();
      lastRescanMs = nowMs;
   }

   roundCounter++;

   static const struct {
      int family;
      int proto;
   } combos[] = {
      { AF_INET,  IPPROTO_TCP },
      { AF_INET,  IPPROTO_UDP },
      { AF_INET6, IPPROTO_TCP },
      { AF_INET6, IPPROTO_UDP },
   };

   bool complete = true;

   for (size_t i = 0;
        i < ARRAYSIZE(combos);
        i++) {

      if (NetLinkNet_dumpOnce(
             combos[i].family,
             combos[i].proto) < 0) {

         complete = false;
         break;
      }
   }

   NetLinkNet_parseProcNetIcmp(PROCDIR "/net/icmp");
   NetLinkNet_parseProcNetIcmp(PROCDIR "/net/icmp6");

   if (complete)
      NetLinkNet_pruneStaleSockets();
   else
      NetLinkNet_closeSocket();
}

bool NetLinkNet_getNetBytes(pid_t pid,
                            unsigned long long* rx,
                            unsigned long long* tx) {
   if (!pidTotals)
      return false;

   PidTotals* totals =
      Hashtable_get(pidTotals,
                    (ht_key_t)pid);

   if (!totals)
      return false;

   *rx = totals->rx;
   *tx = totals->tx;

   return true;
}

bool NetLinkNet_isProcessUnreadable(pid_t pid) {
   if (!unreadablePids)
      return false;

   return Hashtable_get(
      unreadablePids,
      (ht_key_t)pid) != NULL;
}

With some further cleanup that's like 20-25% less code and SOCK_DIAG not depending on libnl …

Comment thread linux/NetMonitor.bpf.c
Comment on lines +6 to +9

eBPF program for per-process network bandwidth accounting.
Compiled at build time with clang into an ELF object which is
embedded into the htop binary and loaded via libbpf.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite the canonical format for this initial header comment … ;-)

Comment thread linux/NetMonitor.c
}
}

NetMonitor_debug("CapEff=0x%llx (need CAP_BPF or CAP_SYS_ADMIN), lockdown=%s, perf_event_paranoid=%s\n",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Providing the mask for CAP_BPF and CAP_SYS_ADMIN might improve UX when debugging this error message … ;-)

Comment thread linux/Platform.c
Comment on lines +1227 to +1228
NetLinkNet_done();
NetMonitor_done();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should release in reverse init order.

Suggested change
NetLinkNet_done();
NetMonitor_done();
NetMonitor_done();
NetLinkNet_done();

Comment thread configure.ac
fi


AC_ARG_ENABLE(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly of consequence for this PR, but possibly split --enable-ebpf (General eBPF setup) and --enable-ebpf-net (ebpf code used for network traffic accounting). Later PRs could then introduce e.g. --enable-ebpf-proc or similar.

--enable-ebpf should be marked auto or implied when any of its sub-features are present (subfeatures failing if eBPF detection fails).

Comment thread htop.1.in
Comment on lines +627 to +629
.B NET_TOTAL (NET TOTAL)
The total network rate, NET_RX + NET_TX (see above).
.TP

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use NET_TOTAL for the wall of text and keep NET_RX and NET_TX as a small not for "just the RX/TX part"? Makes the description more symmetrical.

Comment thread Macros.h
Comment on lines +94 to +96
/* clang ignores the access attribute and emits -Wunknown-attributes for it,
* so only define it when the compiler actually supports it. */
#if defined(HAVE_ATTR_ACCESS) && !defined(__clang__)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the check for this in configure.ac is off somehow?

Comment thread README.md
- dependencies: *libnl-3-dev*(build-time) and *libnl-genl-3-dev*(build-time), at runtime *libnl-3* and *libnl-genl-3* are loaded via `dlopen(3)` if available and requested
- default: *check*

* `--enable-ebpf`:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest

Suggested change
* `--enable-ebpf`:
* `--enable-ebpf-net`:

to accommodate for future extensions.

Daniel Lange added 11 commits October 6, 2026 13:21
These come in three flavours:

Exact:    eBPF Kprobes; these need libbpf, root or CAP_BPF/CAP_SYS_ADMIN
          at runtime. And clang with assorted horsetoppings at build time.
          This measures TCP, UDP and ICMP both IPv4 and IPv6.
          --enable-ebpf-net, no prefix in the columns.

Concise:  NETLINK_SOCK_DIAG based; this needs libnl-3 (might the there from
          delayacct already). This measures TCP and gets a lower bound from
          socket-buffer queue levels for UDP and ICMP).
          --enable-libnl-net, "+" in the columns.

Estimate: This uses read/write byte counters and substracts disk I/O, so
          it is crap when pipes are being used, too.
          Always enabled, "~" in the columns.

Assisted-by: OpenCode Zen
…se I/O rate accounting

Assisted-by: OpenCode Zen
…ing average estimator

... makes for nicer, more continues data points ~ smoother UI
@fasterit
fasterit force-pushed the add-per-process-network-meters branch from 3e4c1ab to 53755b4 Compare October 6, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request Completely new feature requested Linux 🐧 Linux related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants