]> www.infradead.org Git - users/hch/misc.git/commitdiff
smb: client: batch SRV_COPYCHUNK entries to cut round trips
authorHenrique Carvalho <henrique.carvalho@suse.com>
Sat, 4 Oct 2025 02:11:43 +0000 (23:11 -0300)
committerSteve French <stfrench@microsoft.com>
Thu, 9 Oct 2025 15:42:14 +0000 (10:42 -0500)
smb2_copychunk_range() used to send a single SRV_COPYCHUNK per
SRV_COPYCHUNK_COPY IOCTL.

Implement variable Chunks[] array in struct copychunk_ioctl and fill it
with struct copychunk (MS-SMB2 2.2.31.1.1), bounded by server-advertised
limits.

This reduces the number of IOCTL requests for large copies.

While we are at it, rename a couple variables to follow the terminology
used in the specification.

Signed-off-by: Henrique Carvalho <henrique.carvalho@suse.com>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/smb/client/smb2ops.c
fs/smb/client/smb2pdu.h
fs/smb/client/trace.h

index 058050f744c020ab05d031474ac26ee2b000e554..80114292e2c92fd318aad1a2b46a0a847d5afc8e 100644 (file)
@@ -1803,140 +1803,226 @@ free_vars:
        return rc;
 }
 
+/**
+ * calc_chunk_count - calculates the number chunks to be filled in the Chunks[]
+ * array of struct copychunk_ioctl
+ *
+ * @tcon: destination file tcon
+ * @bytes_left: how many bytes are left to copy
+ *
+ * Return: maximum number of chunks with which Chunks[] can be filled.
+ */
+static inline u32
+calc_chunk_count(struct cifs_tcon *tcon, u64 bytes_left)
+{
+       u32 max_chunks = READ_ONCE(tcon->max_chunks);
+       u32 max_bytes_copy = READ_ONCE(tcon->max_bytes_copy);
+       u32 max_bytes_chunk = READ_ONCE(tcon->max_bytes_chunk);
+       u64 need;
+       u32 allowed;
+
+       if (!max_bytes_chunk || !max_bytes_copy || !max_chunks)
+               return 0;
+
+       /* chunks needed for the remaining bytes */
+       need = DIV_ROUND_UP_ULL(bytes_left, max_bytes_chunk);
+       /* chunks allowed per cc request */
+       allowed = DIV_ROUND_UP(max_bytes_copy, max_bytes_chunk);
+
+       return (u32)umin(need, umin(max_chunks, allowed));
+}
+
+/**
+ * smb2_copychunk_range - server-side copy of data range
+ *
+ * @xid: transaction id
+ * @src_file: source file
+ * @dst_file: destination file
+ * @src_off: source file byte offset
+ * @len: number of bytes to copy
+ * @dst_off: destination file byte offset
+ *
+ * Obtains a resume key for @src_file and issues FSCTL_SRV_COPYCHUNK_WRITE
+ * IOCTLs, splitting the request into chunks limited by tcon->max_*.
+ *
+ * Return: @len on success; negative errno on failure.
+ */
 static ssize_t
 smb2_copychunk_range(const unsigned int xid,
-                       struct cifsFileInfo *srcfile,
-                       struct cifsFileInfo *trgtfile, u64 src_off,
-                       u64 len, u64 dest_off)
+                    struct cifsFileInfo *src_file,
+                    struct cifsFileInfo *dst_file,
+                    u64 src_off,
+                    u64 len,
+                    u64 dst_off)
 {
-       int rc;
-       unsigned int ret_data_len;
-       struct copychunk_ioctl *pcchunk;
-       struct copychunk_ioctl_rsp *retbuf = NULL;
+       int rc = 0;
+       unsigned int ret_data_len = 0;
+       struct copychunk_ioctl *cc_req = NULL;
+       struct copychunk_ioctl_rsp *cc_rsp = NULL;
        struct cifs_tcon *tcon;
-       int chunks_copied = 0;
-       bool chunk_sizes_updated = false;
-       ssize_t bytes_written, total_bytes_written = 0;
+       struct copychunk *chunk;
+       u32 chunks, chunk_count, chunk_bytes;
+       u32 copy_bytes, copy_bytes_left;
+       u32 chunks_written, bytes_written;
+       u64 total_bytes_left = len;
+       u64 src_off_prev, dst_off_prev;
+       u32 retries = 0;
+
+       tcon = tlink_tcon(dst_file->tlink);
+
+       trace_smb3_copychunk_enter(xid, src_file->fid.volatile_fid,
+                                  dst_file->fid.volatile_fid, tcon->tid,
+                                  tcon->ses->Suid, src_off, dst_off, len);
+
+retry:
+       chunk_count = calc_chunk_count(tcon, total_bytes_left);
+       if (!chunk_count) {
+               rc = -EOPNOTSUPP;
+               goto out;
+       }
 
-       pcchunk = kmalloc(sizeof(struct copychunk_ioctl), GFP_KERNEL);
-       if (pcchunk == NULL)
-               return -ENOMEM;
+       cc_req = kzalloc(struct_size(cc_req, Chunks, chunk_count), GFP_KERNEL);
+       if (!cc_req) {
+               rc = -ENOMEM;
+               goto out;
+       }
 
-       cifs_dbg(FYI, "%s: about to call request res key\n", __func__);
        /* Request a key from the server to identify the source of the copy */
-       rc = SMB2_request_res_key(xid, tlink_tcon(srcfile->tlink),
-                               srcfile->fid.persistent_fid,
-                               srcfile->fid.volatile_fid, pcchunk);
+       rc = SMB2_request_res_key(xid,
+                                 tlink_tcon(src_file->tlink),
+                                 src_file->fid.persistent_fid,
+                                 src_file->fid.volatile_fid,
+                                 cc_req);
 
-       /* Note: request_res_key sets res_key null only if rc !=0 */
+       /* Note: request_res_key sets res_key null only if rc != 0 */
        if (rc)
-               goto cchunk_out;
+               goto out;
 
-       /* For now array only one chunk long, will make more flexible later */
-       pcchunk->ChunkCount = cpu_to_le32(1);
-       pcchunk->Reserved = 0;
-       pcchunk->Reserved2 = 0;
+       while (total_bytes_left > 0) {
 
-       tcon = tlink_tcon(trgtfile->tlink);
+               /* Store previous offsets to allow rewind */
+               src_off_prev = src_off;
+               dst_off_prev = dst_off;
 
-       trace_smb3_copychunk_enter(xid, srcfile->fid.volatile_fid,
-                                  trgtfile->fid.volatile_fid, tcon->tid,
-                                  tcon->ses->Suid, src_off, dest_off, len);
+               chunks = 0;
+               copy_bytes = 0;
+               copy_bytes_left = umin(total_bytes_left, tcon->max_bytes_copy);
+               while (copy_bytes_left > 0 && chunks < chunk_count) {
+                       chunk = &cc_req->Chunks[chunks++];
 
-       while (len > 0) {
-               pcchunk->SourceOffset = cpu_to_le64(src_off);
-               pcchunk->TargetOffset = cpu_to_le64(dest_off);
-               pcchunk->Length =
-                       cpu_to_le32(min_t(u64, len, tcon->max_bytes_chunk));
+                       chunk->SourceOffset = cpu_to_le64(src_off);
+                       chunk->TargetOffset = cpu_to_le64(dst_off);
+
+                       chunk_bytes = umin(copy_bytes_left, tcon->max_bytes_chunk);
+
+                       chunk->Length = cpu_to_le32(chunk_bytes);
+                       /* Buffer is zeroed, no need to set chunk->Reserved = 0 */
+
+                       src_off += chunk_bytes;
+                       dst_off += chunk_bytes;
+
+                       copy_bytes_left -= chunk_bytes;
+                       copy_bytes += chunk_bytes;
+               }
+
+               cc_req->ChunkCount = cpu_to_le32(chunks);
+               /* Buffer is zeroed, no need to set cc_req->Reserved = 0 */
 
                /* Request server copy to target from src identified by key */
-               kfree(retbuf);
-               retbuf = NULL;
-               rc = SMB2_ioctl(xid, tcon, trgtfile->fid.persistent_fid,
-                       trgtfile->fid.volatile_fid, FSCTL_SRV_COPYCHUNK_WRITE,
-                       (char *)pcchunk, sizeof(struct copychunk_ioctl),
-                       CIFSMaxBufSize, (char **)&retbuf, &ret_data_len);
+               kfree(cc_rsp);
+               cc_rsp = NULL;
+               rc = SMB2_ioctl(xid, tcon, dst_file->fid.persistent_fid,
+                       dst_file->fid.volatile_fid, FSCTL_SRV_COPYCHUNK_WRITE,
+                       (char *)cc_req, struct_size(cc_req, Chunks, chunks),
+                       CIFSMaxBufSize, (char **)&cc_rsp, &ret_data_len);
+
+               if (rc && rc != -EINVAL)
+                       goto out;
+
+               if (unlikely(ret_data_len != sizeof(*cc_rsp))) {
+                       cifs_tcon_dbg(VFS, "Copychunk invalid response: size %u/%zu\n",
+                                     ret_data_len, sizeof(*cc_rsp));
+                       rc = -EIO;
+                       goto out;
+               }
+
+               bytes_written = le32_to_cpu(cc_rsp->TotalBytesWritten);
+               chunks_written = le32_to_cpu(cc_rsp->ChunksWritten);
+               chunk_bytes = le32_to_cpu(cc_rsp->ChunkBytesWritten);
+
                if (rc == 0) {
-                       if (ret_data_len !=
-                                       sizeof(struct copychunk_ioctl_rsp)) {
-                               cifs_tcon_dbg(VFS, "Invalid cchunk response size\n");
-                               rc = -EIO;
-                               goto cchunk_out;
-                       }
-                       if (retbuf->TotalBytesWritten == 0) {
-                               cifs_dbg(FYI, "no bytes copied\n");
-                               rc = -EIO;
-                               goto cchunk_out;
-                       }
-                       /*
-                        * Check if server claimed to write more than we asked
-                        */
-                       if (le32_to_cpu(retbuf->TotalBytesWritten) >
-                           le32_to_cpu(pcchunk->Length)) {
-                               cifs_tcon_dbg(VFS, "Invalid copy chunk response\n");
+                       /* Check if server claimed to write more than we asked */
+                       if (unlikely(!bytes_written || bytes_written > copy_bytes ||
+                                    !chunks_written || chunks_written > chunks)) {
+                               cifs_tcon_dbg(VFS, "Copychunk invalid response: bytes written %u/%u, chunks written %u/%u\n",
+                                             bytes_written, copy_bytes, chunks_written, chunks);
                                rc = -EIO;
-                               goto cchunk_out;
+                               goto out;
                        }
-                       if (le32_to_cpu(retbuf->ChunksWritten) != 1) {
-                               cifs_tcon_dbg(VFS, "Invalid num chunks written\n");
-                               rc = -EIO;
-                               goto cchunk_out;
+
+                       /* Partial write: rewind */
+                       if (bytes_written < copy_bytes) {
+                               u32 delta = copy_bytes - bytes_written;
+
+                               src_off -= delta;
+                               dst_off -= delta;
                        }
-                       chunks_copied++;
-
-                       bytes_written = le32_to_cpu(retbuf->TotalBytesWritten);
-                       src_off += bytes_written;
-                       dest_off += bytes_written;
-                       len -= bytes_written;
-                       total_bytes_written += bytes_written;
-
-                       cifs_dbg(FYI, "Chunks %d PartialChunk %d Total %zu\n",
-                               le32_to_cpu(retbuf->ChunksWritten),
-                               le32_to_cpu(retbuf->ChunkBytesWritten),
-                               bytes_written);
-                       trace_smb3_copychunk_done(xid, srcfile->fid.volatile_fid,
-                               trgtfile->fid.volatile_fid, tcon->tid,
-                               tcon->ses->Suid, src_off, dest_off, len);
-               } else if (rc == -EINVAL) {
-                       if (ret_data_len != sizeof(struct copychunk_ioctl_rsp))
-                               goto cchunk_out;
-
-                       cifs_dbg(FYI, "MaxChunks %d BytesChunk %d MaxCopy %d\n",
-                               le32_to_cpu(retbuf->ChunksWritten),
-                               le32_to_cpu(retbuf->ChunkBytesWritten),
-                               le32_to_cpu(retbuf->TotalBytesWritten));
 
-                       /*
-                        * Check if this is the first request using these sizes,
-                        * (ie check if copy succeed once with original sizes
-                        * and check if the server gave us different sizes after
-                        * we already updated max sizes on previous request).
-                        * if not then why is the server returning an error now
-                        */
-                       if ((chunks_copied != 0) || chunk_sizes_updated)
-                               goto cchunk_out;
-
-                       /* Check that server is not asking us to grow size */
-                       if (le32_to_cpu(retbuf->ChunkBytesWritten) <
-                                       tcon->max_bytes_chunk)
-                               tcon->max_bytes_chunk =
-                                       le32_to_cpu(retbuf->ChunkBytesWritten);
-                       else
-                               goto cchunk_out; /* server gave us bogus size */
+                       total_bytes_left -= bytes_written;
+                       continue;
+               }
 
-                       /* No need to change MaxChunks since already set to 1 */
-                       chunk_sizes_updated = true;
-               } else
-                       goto cchunk_out;
+               /*
+                * Check if server is not asking us to reduce size.
+                *
+                * Note: As per MS-SMB2 2.2.32.1, the values returned
+                * in cc_rsp are not strictly lower than what existed
+                * before.
+                */
+               if (bytes_written < tcon->max_bytes_copy) {
+                       cifs_tcon_dbg(FYI, "Copychunk MaxBytesCopy updated: %u -> %u\n",
+                                     tcon->max_bytes_copy, bytes_written);
+                       tcon->max_bytes_copy = bytes_written;
+               }
+
+               if (chunks_written < tcon->max_chunks) {
+                       cifs_tcon_dbg(FYI, "Copychunk MaxChunks updated: %u -> %u\n",
+                                     tcon->max_chunks, chunks_written);
+                       tcon->max_chunks = chunks_written;
+               }
+
+               if (chunk_bytes < tcon->max_bytes_chunk) {
+                       cifs_tcon_dbg(FYI, "Copychunk MaxBytesChunk updated: %u -> %u\n",
+                                     tcon->max_bytes_chunk, chunk_bytes);
+                       tcon->max_bytes_chunk = chunk_bytes;
+               }
+
+               /* reset to last offsets */
+               if (retries++ < 2) {
+                       src_off = src_off_prev;
+                       dst_off = dst_off_prev;
+                       kfree(cc_req);
+                       cc_req = NULL;
+                       goto retry;
+               }
+
+               break;
        }
 
-cchunk_out:
-       kfree(pcchunk);
-       kfree(retbuf);
-       if (rc)
+out:
+       kfree(cc_req);
+       kfree(cc_rsp);
+       if (rc) {
+               trace_smb3_copychunk_err(xid, src_file->fid.volatile_fid,
+                                        dst_file->fid.volatile_fid, tcon->tid,
+                                        tcon->ses->Suid, src_off, dst_off, len, rc);
                return rc;
-       else
-               return total_bytes_written;
+       } else {
+               trace_smb3_copychunk_done(xid, src_file->fid.volatile_fid,
+                                         dst_file->fid.volatile_fid, tcon->tid,
+                                         tcon->ses->Suid, src_off, dst_off, len);
+               return len;
+       }
 }
 
 static int
index 3c09a58dfd073fc87a77fa9a435fee1785f7698b..101024f8f725687ac0e6aa2efd5017daccc1d165 100644 (file)
@@ -201,16 +201,20 @@ struct resume_key_req {
        char    Context[];      /* ignored, Windows sets to 4 bytes of zero */
 } __packed;
 
+
+struct copychunk {
+       __le64 SourceOffset;
+       __le64 TargetOffset;
+       __le32 Length;
+       __le32 Reserved;
+} __packed;
+
 /* this goes in the ioctl buffer when doing a copychunk request */
 struct copychunk_ioctl {
        char SourceKey[COPY_CHUNK_RES_KEY_SIZE];
-       __le32 ChunkCount; /* we are only sending 1 */
+       __le32 ChunkCount;
        __le32 Reserved;
-       /* array will only be one chunk long for us */
-       __le64 SourceOffset;
-       __le64 TargetOffset;
-       __le32 Length; /* how many bytes to copy */
-       __u32 Reserved2;
+       struct copychunk Chunks[];
 } __packed;
 
 struct copychunk_ioctl_rsp {
index fd650e2afc7629ad6b2ff01cc8302858d20cb91e..28e00c34df1cbb062b3d28676130f04bff7ac300 100644 (file)
@@ -266,7 +266,7 @@ DEFINE_EVENT(smb3_copy_range_err_class, smb3_##name, \
        TP_ARGS(xid, src_fid, target_fid, tid, sesid, src_offset, target_offset, len, rc))
 
 DEFINE_SMB3_COPY_RANGE_ERR_EVENT(clone_err);
-/* TODO: Add SMB3_COPY_RANGE_ERR_EVENT(copychunk_err) */
+DEFINE_SMB3_COPY_RANGE_ERR_EVENT(copychunk_err);
 
 DECLARE_EVENT_CLASS(smb3_copy_range_done_class,
        TP_PROTO(unsigned int xid,