* [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends
@ 2026-10-22 9:25 Huiwen He
2026-10-22 9:25 ` [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers Huiwen He
` (3 more replies)
0 siblings, 4 replies; 10+ messages in thread
From: Huiwen He @ 2026-10-22 9:25 UTC (permalink / raw)
To: linkinjeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
From: Huiwen He <hehuiwen@kylinos.cn>
Hi Namjae,
This series addresses two sources of overhead in ksmbd:
- WRITE scheduling: requests from one connection can accumulate on a
busy per-CPU worker pool even when other CPUs are available. Route
eligible single-channel WRITEs to a CPU-scoped, non-strict unbound
workqueue, while keeping other requests on the per-CPU queue.
- READ sending: TCP copies READ data after the filesystem has filled
the response buffer. Use page-backed buffers and MSG_SPLICE_PAGES
to avoid this copy, with the existing copy path as a fallback.
Test setup:
Server: Linux 7.3.0-rc3+, dual socket, 128 CPUs, tmpfs.
Client: Linux 7.0.0-38-generic, local 4-vCPU VM; file-data caching
bypassed for both protocols.
NFS: Linux nfsd, NFSv4.2/TCP, sec=sys, 32 threads.
SMB: ksmbd and Samba 4.23.6, SMB3.1.1.
fio: 16 jobs, QD32/job, 1 GiB total, libaio; four 30-second runs
after warmup. Mixed: --rw=rw --rwmixread=50, READ + WRITE.
1. Baseline throughput (median MiB/s)
Block Conn Workload NFS Samba ksmbd
----- ---- -------- --------- --------- ---------
4KiB 1 READ 195.59 241.71 292.64
4KiB 1 WRITE 154.68 228.92 219.55
4KiB 1 mixed 172.49 232.42 236.85
4KiB 2 READ 346.23 274.58 401.12
4KiB 2 WRITE 312.62 292.39 325.75
4KiB 2 mixed 329.28 286.86 345.04
4KiB 4 READ 428.34 277.92 463.80
4KiB 4 WRITE 387.19 285.59 415.65
4KiB 4 mixed 402.74 284.52 397.76
64KiB 1 READ 2156.89 1647.55 2152.98
64KiB 1 WRITE 1664.58 1956.34 1808.18
64KiB 1 mixed 1790.19 1854.00 1961.47
64KiB 2 READ 3607.94 1526.36 3345.74
64KiB 2 WRITE 2969.75 2706.88 3223.25
64KiB 2 mixed 3469.13 1990.95 3285.02
64KiB 4 READ 4375.82 1850.92 4213.09
64KiB 4 WRITE 2622.47 2908.68 3016.62
64KiB 4 mixed 4293.94 1857.41 3950.03
1MiB 1 READ 3688.67 2266.23 2142.63
1MiB 1 WRITE 3750.42 3782.38 2184.30
1MiB 1 mixed 3986.36 3108.39 2827.65
1MiB 2 READ 5756.65 2245.04 3960.96
1MiB 2 WRITE 3858.07 3418.41 4063.92
1MiB 2 mixed 5347.24 3151.78 4614.56
1MiB 4 READ 7908.09 2378.32 6750.79
1MiB 4 WRITE 3422.60 4138.59 3566.57
1MiB 4 mixed 6161.38 2676.61 5471.26
2. Combined throughput (median MiB/s)
Before/After: both optimizations off/on in the same kernel.
Block Ch Workload Before After Paired %
----- -- -------- --------- --------- ----------
4KiB 1 READ 292.64 282.21 -3.61%
4KiB 1 WRITE 219.55 192.84 -11.01%
4KiB 1 mixed 236.85 240.15 +0.36%
4KiB 2 READ 401.12 404.66 +1.17%
4KiB 2 WRITE 325.75 342.19 +5.37%
4KiB 2 mixed 345.04 336.34 -2.12%
4KiB 4 READ 463.80 456.10 -1.95%
4KiB 4 WRITE 415.65 417.24 -1.06%
4KiB 4 mixed 397.76 386.93 -2.74%
64KiB 1 READ 2152.98 2558.10 +18.34%
64KiB 1 WRITE 1808.18 2031.92 +11.69%
64KiB 1 mixed 1961.47 2176.16 +9.53%
64KiB 2 READ 3345.74 3591.95 +7.32%
64KiB 2 WRITE 3223.25 3205.70 -0.54%
64KiB 2 mixed 3285.02 3317.22 +2.39%
64KiB 4 READ 4213.09 4427.29 +4.65%
64KiB 4 WRITE 3016.62 3047.30 -0.59%
64KiB 4 mixed 3950.03 3962.34 +0.44%
1MiB 1 READ 2142.63 3353.03 +56.51%
1MiB 1 WRITE 2184.30 3460.33 +58.75%
1MiB 1 mixed 2827.65 3096.88 +9.71%
1MiB 2 READ 3960.96 5546.88 +39.95%
1MiB 2 WRITE 4063.92 4174.43 +2.75%
1MiB 2 mixed 4614.56 4954.98 +7.37%
1MiB 4 READ 6750.79 8311.51 +21.79%
1MiB 4 WRITE 3566.57 3612.88 +0.74%
1MiB 4 mixed 5471.26 5604.89 +3.85%
4 KiB single-channel WRITE regresses by 11.01%. If no better solution
is found, a request-size check could fall back to the per-CPU queue
for small WRITEs.
Thanks,
Huiwen
Huiwen He (4):
workqueue: export attribute and sysfs setup helpers
ksmbd: route WRITE requests to an unbound workqueue
ksmbd: complete TCP frames before reporting send success
ksmbd: send READ payloads with MSG_SPLICE_PAGES
fs/smb/server/connection.c | 1 +
fs/smb/server/connection.h | 6 ++
fs/smb/server/ksmbd_work.c | 141 ++++++++++++++++++++++++++++------
fs/smb/server/ksmbd_work.h | 11 +++
fs/smb/server/server.c | 78 ++++++++++++++++++-
fs/smb/server/smb2pdu.c | 126 +++++++++++++++++++++++++-----
fs/smb/server/smb2pdu.h | 1 +
fs/smb/server/transport_tcp.c | 71 ++++++++++++++++-
fs/smb/server/transport_tcp.h | 1 +
kernel/workqueue.c | 4 +
10 files changed, 395 insertions(+), 45 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers
2026-10-22 9:25 [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends Huiwen He
@ 2026-10-22 9:25 ` Huiwen He
2026-10-23 19:58 ` Tejun Heo
2026-10-22 9:25 ` [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue Huiwen He
` (2 subsequent siblings)
3 siblings, 1 reply; 10+ messages in thread
From: Huiwen He @ 2026-10-22 9:25 UTC (permalink / raw)
To: linkinjeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
From: Huiwen He <hehuiwen@kylinos.cn>
The workqueue attribute helpers are not available to modules.
Export the allocation, application and release helpers for GPL modules.
Also export workqueue_sysfs_register() so a module can apply attributes
before exposing its queue in sysfs.
Signed-off-by: Huiwen He <hehuiwen@kylinos.cn>
---
kernel/workqueue.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 1ae3732a2c51..ec4709395594 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -4822,6 +4822,7 @@ void free_workqueue_attrs(struct workqueue_attrs *attrs)
kfree(attrs);
}
}
+EXPORT_SYMBOL_GPL(free_workqueue_attrs);
/**
* alloc_workqueue_attrs - allocate a workqueue_attrs
@@ -4850,6 +4851,7 @@ struct workqueue_attrs *alloc_workqueue_attrs_noprof(void)
free_workqueue_attrs(attrs);
return NULL;
}
+EXPORT_SYMBOL_GPL(alloc_workqueue_attrs_noprof);
static void copy_workqueue_attrs(struct workqueue_attrs *to,
const struct workqueue_attrs *from)
@@ -5618,6 +5620,7 @@ int apply_workqueue_attrs(struct workqueue_struct *wq,
return ret;
}
+EXPORT_SYMBOL_GPL(apply_workqueue_attrs);
/**
* unbound_wq_update_pwq - update a pwq slot for CPU hot[un]plug
@@ -7746,6 +7749,7 @@ int workqueue_sysfs_register(struct workqueue_struct *wq)
kobject_uevent(&wq_dev->dev.kobj, KOBJ_ADD);
return 0;
}
+EXPORT_SYMBOL_GPL(workqueue_sysfs_register);
/**
* workqueue_sysfs_unregister - undo workqueue_sysfs_register()
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue
2026-10-22 9:25 [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends Huiwen He
2026-10-22 9:25 ` [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers Huiwen He
@ 2026-10-22 9:25 ` Huiwen He
2026-10-24 4:26 ` Namjae Jeon
2026-10-22 9:25 ` [RFC PATCH 3/4] ksmbd: complete TCP frames before reporting send success Huiwen He
2026-10-22 9:25 ` [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES Huiwen He
3 siblings, 1 reply; 10+ messages in thread
From: Huiwen He @ 2026-10-22 9:25 UTC (permalink / raw)
To: linkinjeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
From: Huiwen He <hehuiwen@kylinos.cn>
WRITE requests from one connection can accumulate on a busy per-CPU
pool, limiting throughput even when other CPUs are available.
Route eligible single-channel WRITEs to a CPU-scoped, non-strict
unbound workqueue, while retaining per-CPU execution for other requests.
Tested with fio on a 128-CPU server with a local 4-vCPU client VM
(tmpfs, 16 jobs, QD32 per job, libaio, direct=0, cache=none;
mixed=50/50 R/W). Throughput (MiB/s) and paired changes are medians
of four runs.
A: write_wq=0; B: write_wq=1.
Block Ch Workload A MiB/s B MiB/s B/A change
----- -- --------- --------- --------- ----------
4KiB 1 READ 292.64 290.73 -0.71%
4KiB 1 WRITE 219.55 193.01 -12.03%
4KiB 1 mixed 236.85 238.67 +0.46%
4KiB 2 READ 401.12 404.46 +2.12%
4KiB 2 WRITE 325.75 334.67 +4.22%
4KiB 2 mixed 345.04 338.24 -2.46%
4KiB 4 READ 463.80 453.28 -1.41%
4KiB 4 WRITE 415.65 415.69 -0.14%
4KiB 4 mixed 397.76 396.75 -1.35%
64KiB 1 READ 2152.98 2162.74 +4.12%
64KiB 1 WRITE 1808.18 1976.37 +9.34%
64KiB 1 mixed 1961.47 1974.08 +0.53%
64KiB 2 READ 3345.74 3246.35 -2.93%
64KiB 2 WRITE 3223.25 3222.85 -0.14%
64KiB 2 mixed 3285.02 3283.75 +0.36%
64KiB 4 READ 4213.09 4227.13 +0.34%
64KiB 4 WRITE 3016.62 3089.17 +1.82%
64KiB 4 mixed 3950.03 3877.03 -2.00%
1MiB 1 READ 2142.63 2126.23 -0.99%
1MiB 1 WRITE 2184.30 3452.84 +58.30%
1MiB 1 mixed 2827.65 2961.50 +5.88%
1MiB 2 READ 3960.96 3887.33 -1.99%
1MiB 2 WRITE 4063.92 4158.22 +2.37%
1MiB 2 mixed 4613.54 4631.33 +0.39%
1MiB 4 READ 6750.79 6707.91 -1.49%
1MiB 4 WRITE 3566.57 3557.26 +0.64%
1MiB 4 mixed 5471.26 5444.00 +0.42%
Mixed workloads show no consistent gain: interleaved READs keep later
WRITEs per-CPU until the connection becomes idle.
Signed-off-by: Huiwen He <hehuiwen@kylinos.cn>
---
fs/smb/server/connection.h | 4 ++
fs/smb/server/ksmbd_work.c | 53 ++++++++++++++++++++++++
fs/smb/server/ksmbd_work.h | 2 +
fs/smb/server/server.c | 77 ++++++++++++++++++++++++++++++++++-
fs/smb/server/smb2pdu.c | 6 +++
fs/smb/server/transport_tcp.c | 5 +++
fs/smb/server/transport_tcp.h | 1 +
7 files changed, 147 insertions(+), 1 deletion(-)
diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h
index 63484c8efbbd..a92e0fea0f06 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -59,6 +59,10 @@ struct ksmbd_conn {
struct smb_version_cmds *cmds;
unsigned int max_cmds;
struct mutex srv_mutex;
+ /* RX-only: keep WRITEs per-CPU until idle is observed. */
+ bool write_wq_blocked;
+ /* Sticky across channel removal; accessed with READ/WRITE_ONCE. */
+ bool multichannel_seen;
int status;
unsigned int cli_cap;
bool stop_called;
diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c
index d307aefe0aec..e1b87bc0c949 100644
--- a/fs/smb/server/ksmbd_work.c
+++ b/fs/smb/server/ksmbd_work.c
@@ -7,6 +7,7 @@
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
+#include <linux/module.h>
#include "server.h"
#include "connection.h"
@@ -17,6 +18,23 @@
static struct kmem_cache *work_cache;
static struct workqueue_struct *ksmbd_wq;
+/* Load-time switch for the WRITE request routing policy. */
+bool ksmbd_write_wq_enabled;
+module_param_named(write_wq, ksmbd_write_wq_enabled, bool, 0444);
+MODULE_PARM_DESC(write_wq,
+ "Route eligible WRITEs to an unbound workqueue; load-time only");
+
+static struct workqueue_struct *ksmbd_write_wq;
+
+/* Only the sole receiver calls this for newly admitted requests. */
+bool ksmbd_queue_request(struct ksmbd_work *work, bool use_write_wq)
+{
+ struct workqueue_struct *wq = use_write_wq ? ksmbd_write_wq : ksmbd_wq;
+
+ /* No work/conn access after publication: the consumer may free both. */
+ return queue_work(wq, &work->work);
+}
+
static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt)
{
struct kvec *new;
@@ -110,14 +128,49 @@ int ksmbd_work_pool_init(void)
int ksmbd_workqueue_init(void)
{
+ struct workqueue_attrs *attrs;
+ int ret = -ENOMEM;
+
ksmbd_wq = alloc_workqueue("ksmbd-io", WQ_PERCPU, 0);
if (!ksmbd_wq)
return -ENOMEM;
+ if (!ksmbd_write_wq_enabled)
+ return 0;
+
+ ksmbd_write_wq = alloc_workqueue("ksmbd-write", WQ_UNBOUND, 0);
+ if (!ksmbd_write_wq)
+ goto destroy_io_wq;
+ attrs = alloc_workqueue_attrs();
+ if (!attrs)
+ goto destroy_write_wq;
+ attrs->affn_scope = WQ_AFFN_CPU;
+ attrs->affn_strict = false;
+ ret = apply_workqueue_attrs(ksmbd_write_wq, attrs);
+ free_workqueue_attrs(attrs);
+ if (ret)
+ goto destroy_write_wq;
+
+ /* Set the initial policy before exposing writable sysfs attributes. */
+ ret = workqueue_sysfs_register(ksmbd_write_wq);
+ if (ret)
+ goto destroy_write_wq;
return 0;
+
+destroy_write_wq:
+ destroy_workqueue(ksmbd_write_wq);
+ ksmbd_write_wq = NULL;
+destroy_io_wq:
+ destroy_workqueue(ksmbd_wq);
+ ksmbd_wq = NULL;
+ return ret;
}
void ksmbd_workqueue_destroy(void)
{
+ if (ksmbd_write_wq) {
+ destroy_workqueue(ksmbd_write_wq);
+ ksmbd_write_wq = NULL;
+ }
destroy_workqueue(ksmbd_wq);
ksmbd_wq = NULL;
}
diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h
index 0844aa929f55..9f87b6c4393a 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -156,6 +156,8 @@ int ksmbd_work_pool_init(void);
int ksmbd_workqueue_init(void);
void ksmbd_workqueue_destroy(void);
bool ksmbd_queue_work(struct ksmbd_work *work);
+extern bool ksmbd_write_wq_enabled;
+bool ksmbd_queue_request(struct ksmbd_work *work, bool use_write_wq);
int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
void *aux_buf, unsigned int aux_size);
int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len);
diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c
index 0069d4e6a60a..5347eba916ed 100644
--- a/fs/smb/server/server.c
+++ b/fs/smb/server/server.c
@@ -12,6 +12,7 @@
#include <linux/sysfs.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
+#include <linux/unaligned.h>
#include "server.h"
#include "smb_common.h"
@@ -19,6 +20,7 @@
#include "../common/smb2status.h"
#include "connection.h"
#include "transport_ipc.h"
+#include "transport_tcp.h"
#include "mgmt/user_session.h"
#include "crypto_ctx.h"
#include "auth.h"
@@ -372,6 +374,79 @@ static void handle_ksmbd_work(struct work_struct *wk)
ksmbd_conn_r_count_dec(conn);
}
+enum ksmbd_wq_class {
+ KSMBD_WQ_LOCAL,
+ KSMBD_WQ_WRITE,
+ KSMBD_WQ_BLOCK_WRITE,
+};
+
+/*
+ * Classify a complete RFC1002-framed request for workqueue placement only.
+ * Check bounds before inspecting fields; leave protocol validation to the worker.
+ */
+static enum ksmbd_wq_class ksmbd_smb2_wq_classify(void *buf)
+{
+ const struct smb2_hdr *hdr = smb_get_msg(buf);
+ const struct smb2_write_req *req = (const void *)hdr;
+ unsigned int len = get_rfc1002_len(buf);
+
+ if (len < sizeof(*hdr) || hdr->ProtocolId != SMB2_PROTO_NUMBER ||
+ hdr->NextCommand ||
+ (hdr->Flags & (SMB2_FLAGS_SERVER_TO_REDIR | SMB2_FLAGS_ASYNC_COMMAND |
+ SMB2_FLAGS_RELATED_OPERATIONS | SMB2_FLAGS_SIGNED)))
+ return KSMBD_WQ_BLOCK_WRITE;
+
+ switch (get_unaligned_le16(&hdr->Command)) {
+ case SMB2_WRITE_HE:
+ if (len < sizeof(*req) || !req->Length ||
+ req->Channel != SMB2_CHANNEL_NONE || req->Flags)
+ return KSMBD_WQ_BLOCK_WRITE;
+ return KSMBD_WQ_WRITE;
+ case SMB2_NEGOTIATE_HE:
+ case SMB2_SESSION_SETUP_HE:
+ case SMB2_LOGOFF_HE:
+ case SMB2_TREE_CONNECT_HE:
+ case SMB2_TREE_DISCONNECT_HE:
+ case SMB2_CREATE_HE:
+ case SMB2_CLOSE_HE:
+ case SMB2_FLUSH_HE:
+ case SMB2_CANCEL_HE:
+ case SMB2_ECHO_HE:
+ case SMB2_QUERY_INFO_HE:
+ case SMB2_SET_INFO_HE:
+ /* Keep these commands per-CPU without blocking the WRITE queue. */
+ return KSMBD_WQ_LOCAL;
+ default:
+ /* READ and unclassified traffic also keep later WRITEs per-CPU. */
+ return KSMBD_WQ_BLOCK_WRITE;
+ }
+}
+
+/*
+ * RX-only placement policy. The current request already holds r_count.
+ * READ or unclassified traffic keeps later WRITEs per-CPU until RX
+ * observes r_count == 1. This does not drain or move work already queued.
+ * Extra oplock/notify references can delay rearming, never hasten it.
+ */
+static bool ksmbd_use_write_wq(struct ksmbd_work *work)
+{
+ struct ksmbd_conn *conn = work->conn;
+ enum ksmbd_wq_class wq_class = KSMBD_WQ_BLOCK_WRITE;
+
+ if (!ksmbd_write_wq_enabled)
+ return false;
+
+ if (ksmbd_transport_is_tcp(conn->transport))
+ wq_class = ksmbd_smb2_wq_classify(work->request_buf);
+ if (wq_class == KSMBD_WQ_BLOCK_WRITE)
+ conn->write_wq_blocked = true;
+ else if (conn->write_wq_blocked && atomic_read(&conn->r_count) == 1)
+ conn->write_wq_blocked = false;
+
+ return wq_class == KSMBD_WQ_WRITE && !conn->write_wq_blocked &&
+ !READ_ONCE(conn->multichannel_seen);
+}
+
/**
* queue_ksmbd_work() - queue a smb request to worker thread queue
* for processing smb command and sending response
@@ -403,7 +478,7 @@ static int queue_ksmbd_work(struct ksmbd_conn *conn)
/* update activity on connection */
conn->last_active = jiffies;
INIT_WORK(&work->work, handle_ksmbd_work);
- ksmbd_queue_work(work);
+ ksmbd_queue_request(work, ksmbd_use_write_wq(work));
return 0;
}
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index b7ce67094626..54ecdd096bd2 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -125,6 +125,12 @@ static int register_session_channel(struct ksmbd_session *sess,
if (xa_is_err(old)) {
kfree_sensitive(chann);
rc = xa_err(old);
+ goto out;
+ }
+ if (ksmbd_write_wq_enabled && count) {
+ /* Remember the extra channel even if binding later fails. */
+ xa_for_each(&sess->ksmbd_chann_list, index, chann)
+ WRITE_ONCE(chann->conn->multichannel_seen, true);
}
out:
up_write(&sess->chann_lock);
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index 4968cfc1a572..5a2aad35739d 100644
--- a/fs/smb/server/transport_tcp.c
+++ b/fs/smb/server/transport_tcp.c
@@ -719,3 +719,8 @@ static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = {
.shutdown = ksmbd_tcp_shutdown,
.free_transport = ksmbd_tcp_free_transport,
};
+
+bool ksmbd_transport_is_tcp(struct ksmbd_transport *t)
+{
+ return t->ops == &ksmbd_tcp_transport_ops;
+}
diff --git a/fs/smb/server/transport_tcp.h b/fs/smb/server/transport_tcp.h
index 1e51675ee1b2..4095854ee91d 100644
--- a/fs/smb/server/transport_tcp.h
+++ b/fs/smb/server/transport_tcp.h
@@ -9,6 +9,7 @@
int ksmbd_tcp_set_interfaces(char *ifc_list, int ifc_list_sz);
struct interface *ksmbd_find_netdev_name_iface_list(char *netdev_name);
void ksmbd_free_transport(struct ksmbd_transport *kt);
+bool ksmbd_transport_is_tcp(struct ksmbd_transport *t);
int ksmbd_tcp_init(void);
void ksmbd_tcp_destroy(void);
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH 3/4] ksmbd: complete TCP frames before reporting send success
2026-10-22 9:25 [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends Huiwen He
2026-10-22 9:25 ` [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers Huiwen He
2026-10-22 9:25 ` [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue Huiwen He
@ 2026-10-22 9:25 ` Huiwen He
2026-10-22 9:25 ` [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES Huiwen He
3 siblings, 0 replies; 10+ messages in thread
From: Huiwen He @ 2026-10-22 9:25 UTC (permalink / raw)
To: linkinjeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
From: Huiwen He <hehuiwen@kylinos.cn>
A positive kernel_sendmsg() return may cover only part of an SMB frame.
Reporting success lets the next response follow an incomplete frame.
Retry short sends with the same iterator while holding srv_mutex.
On zero progress or error, shut down the socket before releasing the
lock so no later response can follow an incomplete frame.
Signed-off-by: Huiwen He <hehuiwen@kylinos.cn>
---
fs/smb/server/transport_tcp.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index 5a2aad35739d..1183bb75aa24 100644
--- a/fs/smb/server/transport_tcp.c
+++ b/fs/smb/server/transport_tcp.c
@@ -437,9 +437,22 @@ static int ksmbd_tcp_writev(struct ksmbd_transport *t,
struct msghdr smb_msg = {
.msg_flags = MSG_NOSIGNAL | tx->msg_flags,
};
+ struct socket *sock = TCP_TRANS(t)->sock;
+ int ret;
+
+ iov_iter_kvec(&smb_msg.msg_iter, ITER_SOURCE, tx->iov,
+ tx->iov_cnt, tx->size);
+ /* The caller holds srv_mutex across the entire frame. */
+ while (iov_iter_count(&smb_msg.msg_iter)) {
+ ret = sock_sendmsg(sock, &smb_msg);
+ if (ret <= 0) {
+ /* Never append another SMB frame to an incomplete one. */
+ kernel_sock_shutdown(sock, SHUT_RDWR);
+ return ret ?: -EIO;
+ }
+ }
- return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov,
- tx->iov_cnt, tx->size);
+ return tx->size;
}
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES
2026-10-22 9:25 [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends Huiwen He
` (2 preceding siblings ...)
2026-10-22 9:25 ` [RFC PATCH 3/4] ksmbd: complete TCP frames before reporting send success Huiwen He
@ 2026-10-22 9:25 ` Huiwen He
2026-10-24 4:44 ` Namjae Jeon
3 siblings, 1 reply; 10+ messages in thread
From: Huiwen He @ 2026-10-22 9:25 UTC (permalink / raw)
To: linkinjeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
From: Huiwen He <hehuiwen@kylinos.cn>
The TCP send path copies READ payloads that ksmbd has already read into
its response buffer, adding CPU and memory bandwidth overhead.
Use page-backed READ buffers and MSG_SPLICE_PAGES to avoid this copy.
Keep the existing copy path as a fallback when page-backed sending is
not applicable.
Tested with fio on a 128-CPU server with a local 4-vCPU client VM
(tmpfs, 16 jobs, QD32 per job, libaio, direct=0, cache=none;
mixed=50/50 R/W). Throughput (MiB/s) and paired changes are medians
of four runs.
A: read_splice=0; B: read_splice=1.
Block Ch Workload A MiB/s B MiB/s B/A change
----- -- --------- --------- --------- ----------
4KiB 1 READ 290.73 282.21 -2.94%
4KiB 1 WRITE 193.01 192.84 +1.17%
4KiB 1 mixed 238.67 240.15 +0.55%
4KiB 2 READ 404.46 404.66 +1.17%
4KiB 2 WRITE 334.67 342.19 +0.34%
4KiB 2 mixed 338.24 336.34 -0.38%
4KiB 4 READ 453.28 456.10 -0.87%
4KiB 4 WRITE 415.69 417.24 +0.37%
4KiB 4 mixed 396.75 386.93 -1.41%
64KiB 1 READ 2162.74 2558.10 +19.56%
64KiB 1 WRITE 1976.37 2031.92 +1.26%
64KiB 1 mixed 1974.08 2176.16 +10.13%
64KiB 2 READ 3246.35 3591.95 +10.58%
64KiB 2 WRITE 3222.85 3205.70 -0.66%
64KiB 2 mixed 3283.75 3317.22 +0.86%
64KiB 4 READ 4227.13 4427.29 +5.01%
64KiB 4 WRITE 3089.17 3047.30 -1.36%
64KiB 4 mixed 3877.03 3962.34 +2.09%
1MiB 1 READ 2126.23 3353.03 +59.40%
1MiB 1 WRITE 3452.84 3460.33 +0.25%
1MiB 1 mixed 2961.50 3096.88 +4.64%
1MiB 2 READ 3887.33 5546.88 +42.10%
1MiB 2 WRITE 4158.22 4174.43 +0.18%
1MiB 2 mixed 4335.01 4954.98 +18.71%
1MiB 4 READ 6707.91 8311.51 +23.28%
1MiB 4 WRITE 3557.26 3612.88 +0.38%
1MiB 4 mixed 5444.00 5604.89 +3.12%
Signed-off-by: Huiwen He <hehuiwen@kylinos.cn>
---
fs/smb/server/connection.c | 1 +
fs/smb/server/connection.h | 2 +
fs/smb/server/ksmbd_work.c | 88 ++++++++++++++++++-------
fs/smb/server/ksmbd_work.h | 9 +++
fs/smb/server/server.c | 1 +
fs/smb/server/smb2pdu.c | 120 ++++++++++++++++++++++++++++------
fs/smb/server/smb2pdu.h | 1 +
fs/smb/server/transport_tcp.c | 61 +++++++++++++++--
8 files changed, 235 insertions(+), 48 deletions(-)
diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c
index 4cb92d6599ee..4eb59d81f966 100644
--- a/fs/smb/server/connection.c
+++ b/fs/smb/server/connection.c
@@ -518,6 +518,7 @@ static int __ksmbd_conn_write(struct ksmbd_work *work,
tx->iov = work->iov;
tx->iov_cnt = work->iov_cnt;
tx->size = get_rfc1002_len(work->iov[0].iov_base) + 4;
+ tx->splice_header_len = work->splice_header_len;
tx->need_invalidate_rkey = work->need_invalidate_rkey;
tx->remote_key = work->remote_key;
diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h
index a92e0fea0f06..27a8b17bcbb7 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -166,6 +166,8 @@ struct ksmbd_transport_write {
struct kvec *iov;
int iov_cnt;
int size;
+ /* Prefix to copy into a page fragment before the page-backed last iov. */
+ unsigned int splice_header_len;
bool need_invalidate_rkey;
unsigned int remote_key;
int msg_flags;
diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c
index e1b87bc0c949..9bdd9af1b955 100644
--- a/fs/smb/server/ksmbd_work.c
+++ b/fs/smb/server/ksmbd_work.c
@@ -35,6 +35,47 @@ bool ksmbd_queue_request(struct ksmbd_work *work, bool use_write_wq)
return queue_work(wq, &work->work);
}
+struct aux_read *ksmbd_alloc_read_buf(size_t length, bool allow_splice)
+{
+ struct aux_read *ar;
+ size_t alloc_size = ALIGN(length, 8);
+
+ ar = kzalloc_obj(*ar, KSMBD_DEFAULT_GFP);
+ if (!ar)
+ return NULL;
+
+ if (allow_splice && length) {
+ unsigned int order = get_order(alloc_size);
+
+ /* TCP references to tails must keep the whole allocation alive. */
+ if (order <= MAX_PAGE_ORDER)
+ ar->page = alloc_pages(GFP_KERNEL | __GFP_COMP |
+ __GFP_NORETRY | __GFP_NOWARN, order);
+ }
+ if (ar->page)
+ ar->buf = page_address(ar->page);
+ else
+ ar->buf = kvmalloc(alloc_size, KSMBD_DEFAULT_GFP);
+ if (!ar->buf) {
+ kfree(ar);
+ return NULL;
+ }
+ return ar;
+}
+
+void ksmbd_free_read_buf(struct aux_read *ar)
+{
+ if (!ar)
+ return;
+ if (ar->page) {
+ /* TCP may still own references; never reuse or modify these pages. */
+ put_page(ar->page);
+ } else {
+ kvfree(ar->buf);
+ }
+ kfree(ar);
+}
+
static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt)
{
struct kvec *new;
@@ -92,9 +133,8 @@ void ksmbd_free_work_struct(struct ksmbd_work *work)
kvfree(work->response_buf);
list_for_each_entry_safe(ar, tmp, &work->aux_read_list, entry) {
- kvfree(ar->buf);
list_del(&ar->entry);
- kfree(ar);
+ ksmbd_free_read_buf(ar);
}
kfree(work->tr_buf);
@@ -188,23 +228,14 @@ static inline void __ksmbd_iov_pin(struct ksmbd_work *work, void *ib,
work->iov_cnt++;
}
-static int __ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len,
- void *aux_buf, unsigned int aux_size)
+/* On success, work owns ar, including a buffer with no response payload. */
+int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
+ struct aux_read *ar, unsigned int aux_size)
{
- struct aux_read *ar = NULL;
- int need_iov_cnt = 1;
-
- if (aux_size) {
- need_iov_cnt++;
- ar = kmalloc_obj(struct aux_read, KSMBD_DEFAULT_GFP);
- if (!ar)
- return -ENOMEM;
- }
+ int need_iov_cnt = aux_size ? 2 : 1;
- if (ksmbd_reserve_iov(work, need_iov_cnt)) {
- kfree(ar);
+ if (ksmbd_reserve_iov(work, need_iov_cnt))
return -ENOMEM;
- }
/* Plus rfc_length size on first iov */
if (!work->iov_idx) {
@@ -218,25 +249,36 @@ static int __ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len,
inc_rfc1001_len(work->iov[0].iov_base, len);
if (aux_size) {
- __ksmbd_iov_pin(work, aux_buf, aux_size);
+ __ksmbd_iov_pin(work, ar->buf, aux_size);
inc_rfc1001_len(work->iov[0].iov_base, aux_size);
-
- ar->buf = aux_buf;
- list_add(&ar->entry, &work->aux_read_list);
}
+ if (ar)
+ list_add(&ar->entry, &work->aux_read_list);
return 0;
}
int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len)
{
- return __ksmbd_iov_pin_rsp(work, ib, len, NULL, 0);
+ return ksmbd_iov_pin_rsp_read(work, ib, len, NULL, 0);
}
-int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
+int ksmbd_iov_pin_rsp_data(struct ksmbd_work *work, void *ib, int len,
void *aux_buf, unsigned int aux_size)
{
- return __ksmbd_iov_pin_rsp(work, ib, len, aux_buf, aux_size);
+ struct aux_read *ar;
+ int ret;
+
+ if (!aux_size)
+ return ksmbd_iov_pin_rsp(work, ib, len);
+ ar = kzalloc_obj(*ar, KSMBD_DEFAULT_GFP);
+ if (!ar)
+ return -ENOMEM;
+ ar->buf = aux_buf;
+ ret = ksmbd_iov_pin_rsp_read(work, ib, len, ar, aux_size);
+ if (ret)
+ kfree(ar); /* The caller still owns aux_buf. */
+ return ret;
}
int allocate_interim_rsp_buf(struct ksmbd_work *work)
diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h
index 9f87b6c4393a..26b3d57a8429 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -13,6 +13,7 @@ struct ksmbd_conn;
struct ksmbd_session;
struct ksmbd_tree_connect;
struct ksmbd_file;
+struct page;
#define KSMBD_WORK_INLINE_IOVS 4
@@ -24,6 +25,8 @@ enum {
struct aux_read {
void *buf;
+ /* Non-NULL for a buffer owned through a compound-page reference. */
+ struct page *page;
struct list_head entry;
};
@@ -45,6 +48,8 @@ struct ksmbd_work {
int iov_alloc_cnt;
int iov_cnt;
int iov_idx;
+ /* Final response prefix before a page-backed last iov; zero disables splice. */
+ unsigned int splice_header_len;
struct kvec iov_inline[KSMBD_WORK_INLINE_IOVS];
/* Next cmd hdr in compound req buf*/
@@ -149,6 +154,8 @@ static inline void *ksmbd_req_buf_next(struct ksmbd_work *work)
struct ksmbd_work *ksmbd_alloc_work_struct(void);
void ksmbd_free_work_struct(struct ksmbd_work *work);
+struct aux_read *ksmbd_alloc_read_buf(size_t length, bool allow_splice);
+void ksmbd_free_read_buf(struct aux_read *ar);
void ksmbd_work_pool_destroy(void);
int ksmbd_work_pool_init(void);
@@ -159,6 +166,8 @@ bool ksmbd_queue_work(struct ksmbd_work *work);
extern bool ksmbd_write_wq_enabled;
bool ksmbd_queue_request(struct ksmbd_work *work, bool use_write_wq);
int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
+ struct aux_read *ar, unsigned int aux_size);
+int ksmbd_iov_pin_rsp_data(struct ksmbd_work *work, void *ib, int len,
void *aux_buf, unsigned int aux_size);
int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len);
int allocate_interim_rsp_buf(struct ksmbd_work *work);
diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c
index 5347eba916ed..70cad4e0e8e2 100644
--- a/fs/smb/server/server.c
+++ b/fs/smb/server/server.c
@@ -351,6 +351,7 @@ static void __handle_ksmbd_work(struct ksmbd_work *work,
if (work->sess)
ksmbd_user_session_put(work->sess);
+ smb2_set_read_splice(work);
ksmbd_conn_write(work);
}
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 54ecdd096bd2..3d139c686393 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -14,6 +14,8 @@
#include <linux/ethtool.h>
#include <linux/falloc.h>
#include <linux/mount.h>
+#include <linux/mm.h>
+#include <linux/module.h>
#include <linux/filelock.h>
#include <linux/fileattr.h>
#include <linux/timekeeping.h>
@@ -47,6 +49,29 @@
#include "transport_tcp.h"
#include "compress.h"
+/* Switching affects allocation, not ownership of existing READ buffers. */
+static bool read_splice;
+
+static int read_splice_set(const char *val, const struct kernel_param *kp)
+{
+ bool enabled;
+ int ret;
+
+ ret = kstrtobool(val ?: "1", &enabled);
+ if (!ret)
+ WRITE_ONCE(*(bool *)kp->arg, enabled);
+ return ret;
+}
+
+static const struct kernel_param_ops read_splice_ops = {
+ .flags = KERNEL_PARAM_OPS_FL_NOARG,
+ .set = read_splice_set,
+ .get = param_get_bool,
+};
+module_param_cb(read_splice, &read_splice_ops, &read_splice, 0644);
+__MODULE_PARM_TYPE(read_splice, "bool");
+MODULE_PARM_DESC(read_splice, "Page-backed ordinary TCP READ sends");
+
static void __wbuf(struct ksmbd_work *work, void **req, void **rsp)
{
if (work->next_smb2_rcv_hdr_off) {
@@ -7700,9 +7725,9 @@ static int smb2_get_info_sec(struct ksmbd_work *work,
if (rc)
goto err_out;
- rc = ksmbd_iov_pin_rsp_read(work, (void *)rsp,
- offsetof(struct smb2_query_info_rsp, Buffer),
- pntsd, secdesclen);
+ rc = ksmbd_iov_pin_rsp_data(work, (void *)rsp,
+ offsetof(struct smb2_query_info_rsp, Buffer),
+ pntsd, secdesclen);
err_out:
if (rc) {
rsp->OutputBufferLength = 0;
@@ -8684,7 +8709,7 @@ static noinline int smb2_read_pipe(struct ksmbd_work *work)
rpc_resp->payload_sz);
nbytes = rpc_resp->payload_sz;
- err = ksmbd_iov_pin_rsp_read(work, (void *)rsp,
+ err = ksmbd_iov_pin_rsp_data(work, (void *)rsp,
offsetof(struct smb2_read_rsp, Buffer),
aux_payload_buf, nbytes);
if (err) {
@@ -9004,6 +9029,63 @@ static ssize_t smb2_read_rdma(struct ksmbd_work *work,
return length;
}
+void smb2_set_read_splice(struct ksmbd_work *work)
+{
+ const unsigned int prefix_len = sizeof(__be32);
+ const unsigned int rsp_len = offsetof(struct smb2_read_rsp, Buffer);
+ const unsigned int header_len = prefix_len + rsp_len;
+ const struct smb2_read_rsp *rsp;
+ const struct kvec *payload;
+ const struct aux_read *ar;
+
+ work->splice_header_len = 0;
+ if (!list_is_singular(&work->aux_read_list))
+ return;
+ ar = list_first_entry(&work->aux_read_list, struct aux_read, entry);
+ /* Only eligible TCP READs allocate a page-backed auxiliary buffer. */
+ if (!ar->page)
+ return;
+
+ /* The final response must still be RFC1002 length + READ header + data. */
+ if (work->iov_cnt != 3 || work->iov[0].iov_len != prefix_len ||
+ work->iov[1].iov_len != rsp_len)
+ return;
+ rsp = work->iov[1].iov_base;
+ payload = &work->iov[2];
+
+ /* Signing follows the READ handler; session/tcon refs may be gone. */
+ if (rsp->hdr.Flags & SMB2_FLAGS_SIGNED)
+ return;
+
+ /* Retain the original payload and send only the initialized bytes. */
+ if (payload->iov_base != ar->buf ||
+ !payload->iov_len ||
+ payload->iov_len > page_size(ar->page) ||
+ get_rfc1002_len(work->iov[0].iov_base) + prefix_len !=
+ payload->iov_len + header_len)
+ return;
+
+ /* Do not pin an oversized compound allocation for a short READ. */
+ if (get_order(payload->iov_len) < compound_order(ar->page))
+ return;
+
+ work->splice_header_len = header_len;
+}
+
+static bool smb2_read_splice_eligible(struct ksmbd_work *work,
+ const struct smb2_read_req *req)
+{
+ /* Negotiated signing/encryption capabilities alone do not exclude READs. */
+ return READ_ONCE(read_splice) &&
+ !work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand &&
+ !work->encrypted &&
+ !work->sess->sign && !work->conn->sign &&
+ !(req->hdr.Flags & SMB2_FLAGS_SIGNED) &&
+ !(req->Flags & SMB2_READFLAG_REQUEST_COMPRESSED) &&
+ req->Channel == SMB2_CHANNEL_NONE &&
+ ksmbd_transport_is_tcp(work->conn->transport);
+}
+
/**
* smb2_read() - handler for smb2 read from file
* @work: smb work containing read command buffer
@@ -9013,6 +9095,7 @@ static ssize_t smb2_read_rdma(struct ksmbd_work *work,
int smb2_read(struct ksmbd_work *work)
{
struct ksmbd_conn *conn = work->conn;
+ struct aux_read *ar = NULL;
struct smb2_read_req *req;
struct smb2_read_rsp *rsp;
struct ksmbd_file *fp = NULL;
@@ -9024,7 +9107,6 @@ int smb2_read(struct ksmbd_work *work)
bool is_rdma_channel = false, async_interim = false;
unsigned int max_read_size = conn->vals->max_read_size;
unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID;
- void *aux_payload_buf;
ksmbd_debug(SMB, "Received smb2 read request\n");
@@ -9137,15 +9219,14 @@ int smb2_read(struct ksmbd_work *work)
ksmbd_debug(SMB, "filename %pD, offset %lld, len %zu\n",
fp->filp, offset, length);
- aux_payload_buf = kvmalloc(ALIGN(length, 8), KSMBD_DEFAULT_GFP);
- if (!aux_payload_buf) {
+ ar = ksmbd_alloc_read_buf(length, smb2_read_splice_eligible(work, req));
+ if (!ar) {
err = -ENOMEM;
goto out;
}
- nbytes = ksmbd_vfs_read(work, fp, length, &offset, aux_payload_buf);
+ nbytes = ksmbd_vfs_read(work, fp, length, &offset, ar->buf);
if (nbytes < 0) {
- kvfree(aux_payload_buf);
err = nbytes;
goto out;
}
@@ -9157,10 +9238,10 @@ int smb2_read(struct ksmbd_work *work)
* client. Zero just that tail.
*/
if (nbytes & 7)
- memset(aux_payload_buf + nbytes, 0, ALIGN(nbytes, 8) - nbytes);
+ memset(ar->buf + nbytes, 0, ALIGN(nbytes, 8) - nbytes);
if ((nbytes == 0 && length != 0) || nbytes < mincount) {
- kvfree(aux_payload_buf);
+ ksmbd_free_read_buf(ar);
rsp->hdr.Status = STATUS_END_OF_FILE;
smb2_set_err_rsp(work);
if (async_interim)
@@ -9175,16 +9256,15 @@ int smb2_read(struct ksmbd_work *work)
if (is_rdma_channel == true) {
rdma_transform_len = smb2_prep_rdma_read(work, req,
rsp,
- aux_payload_buf,
+ ar->buf,
nbytes);
if (rdma_transform_len < 0) {
- kvfree(aux_payload_buf);
err = rdma_transform_len;
goto out;
}
/* write data to the client using rdma channel */
remain_bytes = smb2_read_rdma(work, req,
- aux_payload_buf,
+ ar->buf,
nbytes);
if (remain_bytes < 0)
pr_err("RDMA READ transfer failed: session=%llu payload=%zu transform=%d rc=%zd\n",
@@ -9195,8 +9275,8 @@ int smb2_read(struct ksmbd_work *work)
"RDMA READ transfer completed: session=%llu payload=%zu transform=%d\n",
work->sess ? work->sess->id : 0, nbytes,
rdma_transform_len);
- kvfree(aux_payload_buf);
- aux_payload_buf = NULL;
+ ksmbd_free_read_buf(ar);
+ ar = NULL;
nbytes = 0;
if (remain_bytes < 0) {
err = (int)remain_bytes;
@@ -9214,11 +9294,10 @@ int smb2_read(struct ksmbd_work *work)
err = ksmbd_iov_pin_rsp_read(work, (void *)rsp,
offsetof(struct smb2_read_rsp, Buffer) +
rdma_transform_len,
- aux_payload_buf, nbytes);
- if (err) {
- kvfree(aux_payload_buf);
+ ar, nbytes);
+ if (err)
goto out;
- }
+ ar = NULL; /* The response now owns the buffer. */
if (async_interim)
release_async_work(work);
/*
@@ -9234,6 +9313,7 @@ int smb2_read(struct ksmbd_work *work)
return 0;
out:
+ ksmbd_free_read_buf(ar);
if (async_interim)
release_async_work(work);
if (err) {
diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h
index 3f08d1ca5a38..27e60f9ff4db 100644
--- a/fs/smb/server/smb2pdu.h
+++ b/fs/smb/server/smb2pdu.h
@@ -429,6 +429,7 @@ bool smb3_encryption_negotiated(struct ksmbd_conn *conn);
/* smb2 misc functions */
int ksmbd_smb2_check_message(struct ksmbd_work *work);
void smb2_complete_request_open(struct ksmbd_work *work);
+void smb2_set_read_splice(struct ksmbd_work *work);
/* smb2 command handlers */
int smb2_handle_negotiate(struct ksmbd_work *work);
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index 1183bb75aa24..5acf183dab64 100644
--- a/fs/smb/server/transport_tcp.c
+++ b/fs/smb/server/transport_tcp.c
@@ -5,6 +5,7 @@
*/
#include <linux/freezer.h>
+#include <linux/page_frag_cache.h>
#include "smb_common.h"
#include "server.h"
@@ -32,6 +33,8 @@ static int bind_additional_ifaces;
struct tcp_transport {
struct ksmbd_transport transport;
struct socket *sock;
+ /* Serialized by conn->srv_mutex. */
+ struct page_frag_cache tx_frag_cache;
struct kvec *iov;
unsigned int nr_iov;
};
@@ -66,6 +69,7 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
if (!t)
return NULL;
t->sock = client_sk;
+ page_frag_cache_init(&t->tx_frag_cache);
conn = ksmbd_conn_alloc();
if (!conn) {
@@ -100,6 +104,7 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt)
struct tcp_transport *t = TCP_TRANS(kt);
sock_release(t->sock);
+ page_frag_cache_drain(&t->tx_frag_cache);
kfree(t->iov);
kfree(t);
}
@@ -431,6 +436,38 @@ static int ksmbd_tcp_read(struct ksmbd_transport *t, char *buf,
return ksmbd_tcp_readv(TCP_TRANS(t), &iov, 1, to_read, max_retries);
}
+/* The last iov is page-backed; consolidate the prefix into a page fragment. */
+static void *ksmbd_tcp_build_splice_iov(struct tcp_transport *t,
+ const struct ksmbd_transport_write *tx,
+ struct kvec *splice_iov)
+{
+ unsigned int header_len = tx->splice_header_len;
+ const struct kvec *payload;
+ struct iov_iter iter;
+ void *header_frag;
+
+ if (!header_len || header_len > PAGE_SIZE || header_len >= tx->size ||
+ tx->iov_cnt < 2)
+ return NULL;
+ payload = &tx->iov[tx->iov_cnt - 1];
+ if (payload->iov_len != tx->size - header_len)
+ return NULL;
+
+ header_frag = page_frag_alloc(&t->tx_frag_cache, header_len,
+ GFP_KERNEL | __GFP_NORETRY | __GFP_NOWARN);
+ if (!header_frag)
+ return NULL;
+ iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, header_len);
+ if (!copy_from_iter_full(header_frag, header_len, &iter)) {
+ page_frag_free(header_frag);
+ return NULL;
+ }
+ splice_iov[0].iov_base = header_frag;
+ splice_iov[0].iov_len = header_len;
+ splice_iov[1] = *payload;
+ return header_frag;
+}
+
static int ksmbd_tcp_writev(struct ksmbd_transport *t,
const struct ksmbd_transport_write *tx)
{
@@ -438,21 +475,35 @@ static int ksmbd_tcp_writev(struct ksmbd_transport *t,
.msg_flags = MSG_NOSIGNAL | tx->msg_flags,
};
struct socket *sock = TCP_TRANS(t)->sock;
+ struct kvec splice_iov[2];
+ void *header_frag;
int ret;
- iov_iter_kvec(&smb_msg.msg_iter, ITER_SOURCE, tx->iov,
- tx->iov_cnt, tx->size);
- /* The caller holds srv_mutex across the entire frame. */
+ /* The caller holds srv_mutex across preparation and the entire frame. */
+ header_frag = ksmbd_tcp_build_splice_iov(TCP_TRANS(t), tx, splice_iov);
+ if (header_frag) {
+ iov_iter_kvec(&smb_msg.msg_iter, ITER_SOURCE, splice_iov,
+ ARRAY_SIZE(splice_iov), tx->size);
+ smb_msg.msg_flags |= MSG_SPLICE_PAGES;
+ } else {
+ iov_iter_kvec(&smb_msg.msg_iter, ITER_SOURCE, tx->iov,
+ tx->iov_cnt, tx->size);
+ }
while (iov_iter_count(&smb_msg.msg_iter)) {
ret = sock_sendmsg(sock, &smb_msg);
if (ret <= 0) {
/* Never append another SMB frame to an incomplete one. */
kernel_sock_shutdown(sock, SHUT_RDWR);
- return ret ?: -EIO;
+ ret = ret ?: -EIO;
+ goto out;
}
}
- return tx->size;
+ ret = tx->size;
+out:
+ if (header_frag)
+ page_frag_free(header_frag);
+ return ret;
}
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers
2026-10-22 9:25 ` [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers Huiwen He
@ 2026-10-23 19:58 ` Tejun Heo
0 siblings, 0 replies; 10+ messages in thread
From: Tejun Heo @ 2026-10-23 19:58 UTC (permalink / raw)
To: Huiwen He
Cc: linkinjeon, linux-cifs, senozhatsky, tom, chenxiaosong,
linux-kernel
On Thu, Oct 22, 2026 at 05:25:11PM +0800, Huiwen He wrote:
> The workqueue attribute helpers are not available to modules.
>
> Export the allocation, application and release helpers for GPL modules.
> Also export workqueue_sysfs_register() so a module can apply attributes
> before exposing its queue in sysfs.
>
> Signed-off-by: Huiwen He <hehuiwen@kylinos.cn>
Acked-by: Tejun Heo <tj@kernel.org>
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue
2026-10-22 9:25 ` [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue Huiwen He
@ 2026-10-24 4:26 ` Namjae Jeon
2026-10-29 11:52 ` hehuiwen
0 siblings, 1 reply; 10+ messages in thread
From: Namjae Jeon @ 2026-10-24 4:26 UTC (permalink / raw)
To: Huiwen He; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
> @@ -17,6 +18,23 @@
> static struct kmem_cache *work_cache;
> static struct workqueue_struct *ksmbd_wq;
>
> +/* Load-time switch for the WRITE request routing policy. */
> +bool ksmbd_write_wq_enabled;
> +module_param_named(write_wq, ksmbd_write_wq_enabled, bool, 0444);
> +MODULE_PARM_DESC(write_wq,
> + "Route eligible WRITEs to an unbound workqueue; load-time only");
> +
Since this improves performance, why isn't it enabled by default?
Also, I would like this setting to be on/off via ksmbd.conf.
> +enum ksmbd_wq_class {
> + KSMBD_WQ_LOCAL,
> + KSMBD_WQ_WRITE,
> + KSMBD_WQ_BLOCK_WRITE,
> +};
> +
> +/*
> + * Classify a complete RFC1002-framed request for workqueue placement only.
> + * Check bounds before inspecting fields; leave protocol validation to the worker.
> + */
> +static enum ksmbd_wq_class ksmbd_smb2_wq_classify(void *buf)
> +{
> + const struct smb2_hdr *hdr = smb_get_msg(buf);
> + const struct smb2_write_req *req = (const void *)hdr;
> + unsigned int len = get_rfc1002_len(buf);
> +
> + if (len < sizeof(*hdr) || hdr->ProtocolId != SMB2_PROTO_NUMBER ||
> + hdr->NextCommand ||
> + (hdr->Flags & (SMB2_FLAGS_SERVER_TO_REDIR | SMB2_FLAGS_ASYNC_COMMAND |
> + SMB2_FLAGS_RELATED_OPERATIONS | SMB2_FLAGS_SIGNED)))
> + return KSMBD_WQ_BLOCK_WRITE;
> +
> + switch (get_unaligned_le16(&hdr->Command)) {
> + case SMB2_WRITE_HE:
> + if (len < sizeof(*req) || !req->Length ||
> + req->Channel != SMB2_CHANNEL_NONE || req->Flags)
> + return KSMBD_WQ_BLOCK_WRITE;
For 4 KiB single-channel WRITEs, throughput drops from 219.55 to
193.01 MiB/s. Should we add a size check here to keep small WRITEs on
the per-CPU queue?
> + return KSMBD_WQ_WRITE;
> + case SMB2_NEGOTIATE_HE:
> + case SMB2_SESSION_SETUP_HE:
> + case SMB2_LOGOFF_HE:
> + case SMB2_TREE_CONNECT_HE:
> + case SMB2_TREE_DISCONNECT_HE:
> + case SMB2_CREATE_HE:
> + case SMB2_CLOSE_HE:
> + case SMB2_FLUSH_HE:
> + case SMB2_CANCEL_HE:
> + case SMB2_ECHO_HE:
> + case SMB2_QUERY_INFO_HE:
> + case SMB2_SET_INFO_HE:
> + /* Keep these commands per-CPU without blocking the WRITE queue. */
> + return KSMBD_WQ_LOCAL;
> + default:
> + /* READ and unclassified traffic also keep later WRITEs per-CPU. */
> + return KSMBD_WQ_BLOCK_WRITE;
> + }
> +}
> diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
> index b7ce67094626..54ecdd096bd2 100644
> --- a/fs/smb/server/smb2pdu.c
> +++ b/fs/smb/server/smb2pdu.c
> @@ -125,6 +125,12 @@ static int register_session_channel(struct ksmbd_session *sess,
> if (xa_is_err(old)) {
> kfree_sensitive(chann);
> rc = xa_err(old);
> + goto out;
> + }
> + if (ksmbd_write_wq_enabled && count) {
> + /* Remember the extra channel even if binding later fails. */
> + xa_for_each(&sess->ksmbd_chann_list, index, chann)
> + WRITE_ONCE(chann->conn->multichannel_seen, true);
This walks the entire channel list and sets multichannel_seen again
whenever an additional channel is registered. Since the flag is
sticky, all existing channels are already marked when count > 1. Could
we walk the list only when count == 1, marking both the existing and
newly added connections, and update only conn->multichannel_seen when
count > 1? This would avoid the repeated traversal and stores.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES
2026-10-22 9:25 ` [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES Huiwen He
@ 2026-10-24 4:44 ` Namjae Jeon
2026-10-29 15:50 ` hehuiwen
0 siblings, 1 reply; 10+ messages in thread
From: Namjae Jeon @ 2026-10-24 4:44 UTC (permalink / raw)
To: Huiwen He; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
> @@ -35,6 +35,47 @@ bool ksmbd_queue_request(struct ksmbd_work *work, bool use_write_wq)
> return queue_work(wq, &work->work);
> }
>
> +struct aux_read *ksmbd_alloc_read_buf(size_t length, bool allow_splice)
> +{
> + struct aux_read *ar;
> + size_t alloc_size = ALIGN(length, 8);
> +
> + ar = kzalloc_obj(*ar, KSMBD_DEFAULT_GFP);
> + if (!ar)
> + return NULL;
> +
> + if (allow_splice && length) {
> + unsigned int order = get_order(alloc_size);
> +
> + /* TCP references to tails must keep the whole allocation alive. */
> + if (order <= MAX_PAGE_ORDER)
> + ar->page = alloc_pages(GFP_KERNEL | __GFP_COMP |
> + __GFP_NORETRY | __GFP_NOWARN, order);
Order-8 and order-10 allocations can easily fail when memory is
fragmented. Since MSG_SPLICE_PAGES does not require physically
contiguous pages, could we allocate individual pages and use a bvec
iterator instead? Please see drivers/nvme/host/tcp.c as an example.
> + }
> + if (ar->page)
> + ar->buf = page_address(ar->page);
> + else
> + ar->buf = kvmalloc(alloc_size, KSMBD_DEFAULT_GFP);
> + if (!ar->buf) {
> + kfree(ar);
> + return NULL;
> + }
> + return ar;
> +}
> +
> @@ -47,6 +49,29 @@
> #include "transport_tcp.h"
> #include "compress.h"
>
> +/* Switching affects allocation, not ownership of existing READ buffers. */
> +static bool read_splice;
> +
> +static int read_splice_set(const char *val, const struct kernel_param *kp)
> +{
> + bool enabled;
> + int ret;
> +
> + ret = kstrtobool(val ?: "1", &enabled);
> + if (!ret)
> + WRITE_ONCE(*(bool *)kp->arg, enabled);
> + return ret;
> +}
> +
> +static const struct kernel_param_ops read_splice_ops = {
> + .flags = KERNEL_PARAM_OPS_FL_NOARG,
> + .set = read_splice_set,
> + .get = param_get_bool,
> +};
> +module_param_cb(read_splice, &read_splice_ops, &read_splice, 0644);
> +__MODULE_PARM_TYPE(read_splice, "bool");
> +MODULE_PARM_DESC(read_splice, "Page-backed ordinary TCP READ sends");
Can we set enable/disable it via ksmbd.conf also ?
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue
2026-10-24 4:26 ` Namjae Jeon
@ 2026-10-29 11:52 ` hehuiwen
0 siblings, 0 replies; 10+ messages in thread
From: hehuiwen @ 2026-10-29 11:52 UTC (permalink / raw)
To: Namjae Jeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
Hi Namjae,
在 2026/10/24 12:26, Namjae Jeon 写道:
> Since this improves performance, why isn't it enabled by default?
> Also, I would like this setting to be on/off via ksmbd.conf.
I left it off by default for now, since I've noticed some
regressions.I'm testing more cases and trying to find a general
fix. If those regressions can't be avoided, a ksmbd.conf option
would be needed.
> For 4 KiB single-channel WRITEs, throughput drops from 219.55 to
> 193.01 MiB/s. Should we add a size check here to keep small WRITEs on
> the per-CPU queue?
I tried keeping small WRITEs on PERCPU and routing only larger ones
to UNBOUND. However, this did not work well for workloads with mixed
request sizes. For example, a workload mixing 8 KiB and 64 KiB WRITEs
still regressed. Small and large WRITEs share the same connection and
send lock, so a size check alone does not avoid contention.
I then changed the send path to let one worker send several completed
WRITE responses together, without waiting for more responses. This seems
to work better:
Single-channel WRITE, 16 jobs, QD32 per job, tmpfs on a 128-CPU server,
Throughput is in MiB/s.
Size PERCPU UNBOUND+batch Change
4 KiB 228.12 248.89 +6.96%
64 KiB 1797.26 2091.17 +15.34%
1 MiB 2188.42 3330.72 +53.47%
However, I found that there is still a performance regression at QD1.
1 MiB WRITE, one channel, one job:
With schedutil:
QD PERCPU UNBOUND+batch Change
1 1945.94 1021.28 -47.52%
2 2844.66 2567.18 -9.75%
4 2338.03 3547.42 +51.73%
Performance governor on all host CPUs:
QD PERCPU UNBOUND+batch Change
1 1974.35 1785.16 -9.58%
2 2904.12 3038.52 +4.63%
4 2418.45 3659.93 +51.33%
CPU frequency scaling makes the QD1 regression larger.
The main challenge seems to be balancing CPU locality and parallelism.
Keeping the work local helps QD1, while allowing it to run on another
CPU helps when receiving and processing requests in parallel. I have
not yet found a suitable policy that works well for both.
Any ideas about this?
> This walks the entire channel list and sets multichannel_seen again
> whenever an additional channel is registered. Since the flag is
> sticky, all existing channels are already marked when count > 1. Could
> we walk the list only when count == 1, marking both the existing and
> newly added connections, and update only conn->multichannel_seen when
> count > 1? This would avoid the repeated traversal and stores.
Agreed. Thanks for pointing this out.
Best regards,
Huiwen
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES
2026-10-24 4:44 ` Namjae Jeon
@ 2026-10-29 15:50 ` hehuiwen
0 siblings, 0 replies; 10+ messages in thread
From: hehuiwen @ 2026-10-29 15:50 UTC (permalink / raw)
To: Namjae Jeon; +Cc: linux-cifs, senozhatsky, tom, chenxiaosong, tj, linux-kernel
在 2026/10/24 12:44, Namjae Jeon 写道:
> Order-8 and order-10 allocations can easily fail when memory is
> fragmented. Since MSG_SPLICE_PAGES does not require physically
> contiguous pages, could we allocate individual pages and use a bvec
> iterator instead? Please see drivers/nvme/host/tcp.c as an example.
Yeah, I agree with this. But I tried individual pages with a bvec
iterator. It was about 9% slower than the high-order page version.
Anyway, it still faster than the original copy-based implementation.
Thanks,
Huiwen
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-10-29 15:50 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-22 9:25 [RFC PATCH 0/4] ksmbd: improve WRITE scheduling and TCP READ sends Huiwen He
2026-10-22 9:25 ` [RFC PATCH 1/4] workqueue: export attribute and sysfs setup helpers Huiwen He
2026-10-23 19:58 ` Tejun Heo
2026-10-22 9:25 ` [RFC PATCH 2/4] ksmbd: route WRITE requests to an unbound workqueue Huiwen He
2026-10-24 4:26 ` Namjae Jeon
2026-10-29 11:52 ` hehuiwen
2026-10-22 9:25 ` [RFC PATCH 3/4] ksmbd: complete TCP frames before reporting send success Huiwen He
2026-10-22 9:25 ` [RFC PATCH 4/4] ksmbd: send READ payloads with MSG_SPLICE_PAGES Huiwen He
2026-10-24 4:44 ` Namjae Jeon
2026-10-29 15:50 ` hehuiwen
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.