[PATCH v2 2/2] nfsd: report per-client NFSv4 state usage through Netlink
From: Prabhakar Pujeri
Date: Mon Aug 31 2026 - 05:47:06 EST
A client can retain substantial server-side state even when its lease
and callback channel look healthy. Monitoring tools need compact
per-client totals to identify which client owns that state before doing
a detailed inspection through the nfsd filesystem.
Extend client-get replies with separate counts for sessions and open,
lock, delegation, and layout stateid records. A lock stateid represents
a lock-owner/file pair and can cover multiple byte-range locks, so
document that these are record counts rather than protocol operation or
byte-range counts.
Maintain u64 counters under cl_lock at the existing session and stateid
publish and removal points. The dump takes an O(1) snapshot instead of
walking an unbounded per-client IDR while holding the spinlock. Stateid
counters are decremented only at final IDR removal, preserving the
existing record-count semantics for retained stateids.
Encode the counters as variable-width Netlink uint attributes, which use
four bytes for values that fit in u32 and eight bytes otherwise.
Assisted-by: LLM sparse
Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@xxxxxxxx>
---
Changes since v1:
- replace unbounded session-list and stateid-IDR walks under cl_lock with
maintained O(1) u64 counters
- use variable-width Netlink uint attributes for the counters
v1: https://lore.kernel.org/r/245a1ee42d054e280629c9640c8d8225b45639d0.1787638668.git.prabhakar.pujeri@xxxxxxxx
.../admin-guide/nfs/nfsd-admin-interfaces.rst | 6 ++
Documentation/netlink/specs/nfsd.yaml | 27 +++++-
fs/nfsd/nfs4layouts.c | 2 +-
fs/nfsd/nfs4state.c | 95 +++++++++++++++++--
fs/nfsd/state.h | 7 ++
include/uapi/linux/nfsd_netlink.h | 5 +
6 files changed, 130 insertions(+), 12 deletions(-)
diff --git a/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst b/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
index de2a54025874..199141923159 100644
--- a/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
+++ b/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
@@ -38,6 +38,12 @@ by its server-generated client ID and transport address, then reports its
minor version, client and callback states, signed lease time remaining, and
whether an NFSv4.1 or later client sent RECLAIM_COMPLETE.
+The dump also reports separate counts for sessions and open, lock, delegation,
+and layout stateids. A lock stateid represents state for one lock owner and
+file, not necessarily one byte-range lock. NFSD maintains these counters as
+state changes, so reporting a client does not walk its session or stateid
+tables.
+
Clients can change between messages. If that can make the dump skip or repeat
a record, the kernel sets ``NLM_F_DUMP_INTR`` and userspace should retry.
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 9207a96fe594..8fa1d6925515 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -490,6 +490,26 @@ attribute-sets:
type: u32
enum: callback-state
doc: Health of the client's callback channel.
+ -
+ name: sessions
+ type: uint
+ doc: Number of NFSv4.1 or later sessions owned by the client.
+ -
+ name: open-stateids
+ type: uint
+ doc: Number of open stateid records owned by the client.
+ -
+ name: lock-stateids
+ type: uint
+ doc: Number of lock-owner/file stateid records, not byte-range locks.
+ -
+ name: delegation-stateids
+ type: uint
+ doc: Number of delegation stateid records owned by the client.
+ -
+ name: layout-stateids
+ type: uint
+ doc: Number of pNFS layout stateid records owned by the client.
operations:
list:
@@ -704,7 +724,7 @@ operations:
- proc4cb-ops
-
name: client-get
- doc: dump NFSv4 clients
+ doc: dump NFSv4 clients and their current state usage
attribute-set: client
flags: [admin-perm]
dump:
@@ -720,6 +740,11 @@ operations:
- lease-remaining
- reclaim-complete
- callback-state
+ - sessions
+ - open-stateids
+ - lock-stateids
+ - delegation-stateids
+ - layout-stateids
mcast-groups:
list:
diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c
index 4187202f9acc..adb9f33ef7ae 100644
--- a/fs/nfsd/nfs4layouts.c
+++ b/fs/nfsd/nfs4layouts.c
@@ -272,7 +272,7 @@ nfsd4_alloc_layout_stateid(struct nfsd4_compound_state *cstate,
}
spin_lock(&clp->cl_lock);
- stp->sc_type = SC_TYPE_LAYOUT;
+ nfs4_set_stid_type_locked(stp, SC_TYPE_LAYOUT);
list_add(&ls->ls_perclnt, &clp->cl_lo_states);
spin_unlock(&clp->cl_lock);
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index d96e73275b74..44a724ed05c0 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -984,6 +984,54 @@ struct nfs4_stid *nfs4_alloc_stid(struct nfs4_client *cl, struct kmem_cache *sla
return NULL;
}
+static u64 *nfs4_stid_counter(struct nfs4_client *clp, unsigned short type)
+{
+ switch (type) {
+ case SC_TYPE_OPEN:
+ return &clp->cl_open_stateid_count;
+ case SC_TYPE_LOCK:
+ return &clp->cl_lock_stateid_count;
+ case SC_TYPE_DELEG:
+ return &clp->cl_delegation_stateid_count;
+ case SC_TYPE_LAYOUT:
+ return &clp->cl_layout_stateid_count;
+ default:
+ return NULL;
+ }
+}
+
+void nfs4_set_stid_type_locked(struct nfs4_stid *stid, unsigned short type)
+{
+ struct nfs4_client *clp = stid->sc_client;
+ u64 *counter;
+
+ lockdep_assert_held(&clp->cl_lock);
+
+ if (WARN_ON_ONCE(stid->sc_type))
+ return;
+
+ counter = nfs4_stid_counter(clp, type);
+ if (WARN_ON_ONCE(!counter && type != SC_TYPE_COPY))
+ return;
+
+ stid->sc_type = type;
+ if (counter)
+ (*counter)++;
+}
+
+static void nfs4_remove_stid_locked(struct nfs4_stid *stid)
+{
+ struct nfs4_client *clp = stid->sc_client;
+ u64 *counter;
+
+ lockdep_assert_held(&clp->cl_lock);
+
+ counter = nfs4_stid_counter(clp, stid->sc_type);
+ if (counter && !WARN_ON_ONCE(!*counter))
+ (*counter)--;
+ idr_remove(&clp->cl_stateids, stid->sc_stateid.si_opaque.so_id);
+}
+
/*
* Publish a COPY_NOTIFY stateid in nn->s2s_cp_stateids and link it onto the
* parent's sc_cp_list. That IDR holds only COPY_NOTIFY stateids.
@@ -1046,9 +1094,11 @@ struct nfsd4_async_copy *nfs4_alloc_copy_stid(struct nfs4_client *clp)
stid = nfs4_alloc_stid(clp, async_copy_slab, nfsd4_free_async_copy_stid);
if (!stid)
return NULL;
- stid->sc_type = SC_TYPE_COPY;
/* RFC 7862 Section 4.8: a copy offload stateid's seqid MUST NOT be 0 */
stid->sc_stateid.si_generation = 1;
+ spin_lock(&clp->cl_lock);
+ nfs4_set_stid_type_locked(stid, SC_TYPE_COPY);
+ spin_unlock(&clp->cl_lock);
return container_of(stid, struct nfsd4_async_copy, cp_stid);
}
@@ -1386,7 +1436,7 @@ nfs4_put_stid(struct nfs4_stid *s)
wake_up_all(&close_wq);
return;
}
- idr_remove(&clp->cl_stateids, s->sc_stateid.si_opaque.so_id);
+ nfs4_remove_stid_locked(s);
if (s->sc_status & SC_STATUS_ADMIN_REVOKED)
atomic_dec(&s->sc_client->cl_admin_revoked);
/* Read under cl_lock to serialize with drop_stid_export(). */
@@ -1530,7 +1580,7 @@ hash_delegation_locked(struct nfs4_delegation *dp, struct nfs4_file *fp)
if (nfs4_delegation_exists(clp, fp))
return -EAGAIN;
refcount_inc(&dp->dl_stid.sc_count);
- dp->dl_stid.sc_type = SC_TYPE_DELEG;
+ nfs4_set_stid_type_locked(&dp->dl_stid, SC_TYPE_DELEG);
list_add(&dp->dl_perfile, &fp->fi_delegations);
list_add(&dp->dl_perclnt, &clp->cl_delegations);
clp->cl_deleg_count++;
@@ -1814,7 +1864,7 @@ static void put_ol_stateid_locked(struct nfs4_ol_stateid *stp,
return;
}
- idr_remove(&clp->cl_stateids, s->sc_stateid.si_opaque.so_id);
+ nfs4_remove_stid_locked(s);
if (s->sc_status & SC_STATUS_ADMIN_REVOKED)
atomic_dec(&s->sc_client->cl_admin_revoked);
list_add(&stp->st_locks, reaplist);
@@ -2634,6 +2684,7 @@ static void init_session(struct svc_rqst *rqstp, struct nfsd4_session *new, stru
list_add(&new->se_hash, &nn->sessionid_hashtbl[idx]);
spin_lock(&clp->cl_lock);
list_add(&new->se_perclnt, &clp->cl_sessions);
+ clp->cl_session_count++;
spin_unlock(&clp->cl_lock);
spin_lock(&nfsd_session_list_lock);
@@ -2707,9 +2758,11 @@ unhash_session(struct nfsd4_session *ses)
lockdep_assert_held(&nn->client_lock);
list_del(&ses->se_hash);
- spin_lock(&ses->se_client->cl_lock);
+ spin_lock(&clp->cl_lock);
list_del(&ses->se_perclnt);
- spin_unlock(&ses->se_client->cl_lock);
+ if (!WARN_ON_ONCE(!clp->cl_session_count))
+ clp->cl_session_count--;
+ spin_unlock(&clp->cl_lock);
spin_lock(&nfsd_session_list_lock);
list_del(&ses->se_all_sessions);
atomic_dec(&nfsd_total_sessions);
@@ -2822,9 +2875,9 @@ free_client(struct nfs4_client *clp)
{
LIST_HEAD(reaplist);
- /* client_info_show() walks cl_sessions under cl_lock */
spin_lock(&clp->cl_lock);
list_splice_init(&clp->cl_sessions, &reaplist);
+ clp->cl_session_count = 0;
spin_unlock(&clp->cl_lock);
while (!list_empty(&reaplist)) {
struct nfsd4_session *ses;
@@ -3255,6 +3308,11 @@ struct nfsd4_nl_client {
u32 minor_version;
u32 state;
u32 callback_state;
+ u64 sessions;
+ u64 open_stateids;
+ u64 lock_stateids;
+ u64 delegation_stateids;
+ u64 layout_stateids;
bool reclaim_complete;
};
@@ -3343,6 +3401,14 @@ static void nfsd4_nl_client_snapshot(struct nfsd_net *nn,
nfsd4_nl_callback_state(READ_ONCE(clp->cl_cb_state));
client->reclaim_complete =
test_bit(NFSD4_CLIENT_RECLAIM_COMPLETE, &clp->cl_flags);
+
+ spin_lock(&clp->cl_lock);
+ client->sessions = clp->cl_session_count;
+ client->open_stateids = clp->cl_open_stateid_count;
+ client->lock_stateids = clp->cl_lock_stateid_count;
+ client->delegation_stateids = clp->cl_delegation_stateid_count;
+ client->layout_stateids = clp->cl_layout_stateid_count;
+ spin_unlock(&clp->cl_lock);
}
static int
@@ -3403,7 +3469,16 @@ static int nfsd4_nl_client_compose_msg(struct sk_buff *skb,
(client->reclaim_complete &&
nla_put_flag(skb, NFSD_A_CLIENT_RECLAIM_COMPLETE)) ||
nla_put_u32(skb, NFSD_A_CLIENT_CALLBACK_STATE,
- client->callback_state))
+ client->callback_state) ||
+ nla_put_uint(skb, NFSD_A_CLIENT_SESSIONS, client->sessions) ||
+ nla_put_uint(skb, NFSD_A_CLIENT_OPEN_STATEIDS,
+ client->open_stateids) ||
+ nla_put_uint(skb, NFSD_A_CLIENT_LOCK_STATEIDS,
+ client->lock_stateids) ||
+ nla_put_uint(skb, NFSD_A_CLIENT_DELEGATION_STATEIDS,
+ client->delegation_stateids) ||
+ nla_put_uint(skb, NFSD_A_CLIENT_LAYOUT_STATEIDS,
+ client->layout_stateids))
goto err_cancel;
genlmsg_end(skb, hdr);
@@ -6251,7 +6326,7 @@ init_open_stateid(struct nfs4_file *fp, struct nfsd4_open *open)
open->op_stp = NULL;
refcount_inc(&stp->st_stid.sc_count);
- stp->st_stid.sc_type = SC_TYPE_OPEN;
+ nfs4_set_stid_type_locked(&stp->st_stid, SC_TYPE_OPEN);
INIT_LIST_HEAD(&stp->st_locks);
stp->st_stateowner = nfs4_get_stateowner(&oo->oo_owner);
get_nfs4_file(fp);
@@ -9533,7 +9608,7 @@ init_lock_stateid(struct nfs4_ol_stateid *stp, struct nfs4_lockowner *lo,
if (retstp)
goto out_found;
refcount_inc(&stp->st_stid.sc_count);
- stp->st_stid.sc_type = SC_TYPE_LOCK;
+ nfs4_set_stid_type_locked(&stp->st_stid, SC_TYPE_LOCK);
stp->st_stateowner = nfs4_get_stateowner(&lo->lo_owner);
get_nfs4_file(fp);
stp->st_stid.sc_file = fp;
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index cd9294f024bb..3fe130ce5de2 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -610,6 +610,12 @@ struct nfs4_client {
/* for nfs41 */
struct list_head cl_sessions;
+ /* State usage counters, protected by cl_lock. */
+ u64 cl_session_count;
+ u64 cl_open_stateid_count;
+ u64 cl_lock_stateid_count;
+ u64 cl_delegation_stateid_count;
+ u64 cl_layout_stateid_count;
struct nfsd4_clid_slot cl_cs_slot; /* create_session slot */
u32 cl_exchange_flags;
/* number of rpc's in progress over an associated session: */
@@ -923,6 +929,7 @@ __be32 nfsd4_lookup_stateid(struct nfsd4_compound_state *cstate,
struct nfs4_stid **s, struct nfsd_net *nn);
struct nfs4_stid *nfs4_alloc_stid(struct nfs4_client *cl, struct kmem_cache *slab,
void (*sc_free)(struct nfs4_stid *));
+void nfs4_set_stid_type_locked(struct nfs4_stid *stid, unsigned short type);
struct nfsd4_async_copy *nfs4_alloc_copy_stid(struct nfs4_client *clp);
struct nfs4_cpntf_state *nfs4_alloc_init_cpntf_state(struct nfsd_net *nn,
struct nfs4_stid *p_stid);
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index 715acb4104c1..d97783335ac2 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -292,6 +292,11 @@ enum {
NFSD_A_CLIENT_LEASE_REMAINING,
NFSD_A_CLIENT_RECLAIM_COMPLETE,
NFSD_A_CLIENT_CALLBACK_STATE,
+ NFSD_A_CLIENT_SESSIONS,
+ NFSD_A_CLIENT_OPEN_STATEIDS,
+ NFSD_A_CLIENT_LOCK_STATEIDS,
+ NFSD_A_CLIENT_DELEGATION_STATEIDS,
+ NFSD_A_CLIENT_LAYOUT_STATEIDS,
__NFSD_A_CLIENT_MAX,
NFSD_A_CLIENT_MAX = (__NFSD_A_CLIENT_MAX - 1)
--
2.54.0