1 //! This module contains specializations that can offload `io::copy()` operations on file descriptor
2 //! containing types (`File`, `TcpStream`, etc.) to more efficient syscalls than `read(2)` and `write(2)`.
4 //! Specialization is only applied to wholly std-owned types so that user code can't observe
5 //! that the `Read` and `Write` traits are not used.
7 //! Since a copy operation involves a reader and writer side where each can consist of different types
8 //! and also involve generic wrappers (e.g. `Take`, `BufReader`) it is not practical to specialize
9 //! a single method on all possible combinations.
11 //! Instead readers and writers are handled separately by the `CopyRead` and `CopyWrite` specialization
12 //! traits and then specialized on by the `Copier::copy` method.
14 //! `Copier` uses the specialization traits to unpack the underlying file descriptors and
15 //! additional prerequisites and constraints imposed by the wrapper types.
17 //! Once it has obtained all necessary pieces and brought any wrapper types into a state where they
18 //! can be safely bypassed it will attempt to use the `copy_file_range(2)`,
19 //! `sendfile(2)` or `splice(2)` syscalls to move data directly between file descriptors.
20 //! Since those syscalls have requirements that cannot be fully checked in advance and
21 //! gathering additional information about file descriptors would require additional syscalls
22 //! anyway it simply attempts to use them one after another (guided by inaccurate hints) to
23 //! figure out which one works and and falls back to the generic read-write copy loop if none of them
25 //! Once a working syscall is found for a pair of file descriptors it will be called in a loop
26 //! until the copy operation is completed.
28 //! Advantages of using these syscalls:
30 //! * fewer context switches since reads and writes are coalesced into a single syscall
31 //! and more bytes are transferred per syscall. This translates to higher throughput
32 //! and fewer CPU cycles, at least for sufficiently large transfers to amortize the initial probing.
33 //! * `copy_file_range` creates reflink copies on CoW filesystems, thus moving less data and
34 //! consuming less disk space
35 //! * `sendfile` and `splice` can perform zero-copy IO under some circumstances while
36 //! a naive copy loop would move every byte through the CPU.
40 //! * copy operations smaller than the default buffer size can under some circumstances, especially
41 //! on older kernels, incur more syscalls than the naive approach would. As mentioned above
42 //! the syscall selection is guided by hints to minimize this possibility but they are not perfect.
43 //! * optimizations only apply to std types. If a user adds a custom wrapper type, e.g. to report
44 //! progress, they can hit a performance cliff.
48 use crate::convert::TryInto;
49 use crate::fs::{File, Metadata};
50 use crate::io::copy::generic_copy;
52 BufRead, BufReader, BufWriter, Error, Read, Result, StderrLock, StdinLock, StdoutLock, Take,
55 use crate::mem::ManuallyDrop;
56 use crate::net::TcpStream;
57 use crate::os::unix::fs::FileTypeExt;
58 use crate::os::unix::io::{AsRawFd, FromRawFd, RawFd};
59 use crate::process::{ChildStderr, ChildStdin, ChildStdout};
61 use crate::sync::atomic::{AtomicBool, Ordering};
67 pub(crate) fn copy_spec<R: Read + ?Sized, W: Write + ?Sized>(
71 let copier = Copier { read, write };
72 SpecCopy::copy(copier)
75 /// This type represents either the inferred `FileType` of a `RawFd` based on the source
76 /// type from which it was extracted or the actual metadata
78 /// The methods on this type only provide hints, due to `AsRawFd` and `FromRawFd` the inferred
79 /// type may be wrong.
81 /// We obtained the FD from a type that can contain any type of `FileType` and queried the metadata
82 /// because it is cheaper than probing all possible syscalls (reader side)
86 /// We don't have any metadata, e.g. because the original type was `File` which can represent
87 /// any `FileType` and we did not query the metadata either since it did not seem beneficial
93 fn maybe_fifo(&self) -> bool {
95 FdMeta::Metadata(meta) => meta.file_type().is_fifo(),
96 FdMeta::Socket => false,
98 FdMeta::NoneObtained => true,
102 fn potential_sendfile_source(&self) -> bool {
104 // procfs erronously shows 0 length on non-empty readable files.
105 // and if a file is truly empty then a `read` syscall will determine that and skip the write syscall
106 // thus there would be benefit from attempting sendfile
107 FdMeta::Metadata(meta)
108 if meta.file_type().is_file() && meta.len() > 0
109 || meta.file_type().is_block_device() =>
117 fn copy_file_range_candidate(&self) -> bool {
119 // copy_file_range will fail on empty procfs files. `read` can determine whether EOF has been reached
120 // without extra cost and skip the write, thus there is no benefit in attempting copy_file_range
121 FdMeta::Metadata(meta) if meta.is_file() && meta.len() > 0 => true,
122 FdMeta::NoneObtained => true,
128 struct CopyParams(FdMeta, Option<RawFd>);
130 struct Copier<'a, 'b, R: Read + ?Sized, W: Write + ?Sized> {
136 fn copy(self) -> Result<u64>;
139 impl<R: Read + ?Sized, W: Write + ?Sized> SpecCopy for Copier<'_, '_, R, W> {
140 default fn copy(self) -> Result<u64> {
141 generic_copy(self.read, self.write)
145 impl<R: CopyRead, W: CopyWrite> SpecCopy for Copier<'_, '_, R, W> {
146 fn copy(self) -> Result<u64> {
147 let (reader, writer) = (self.read, self.write);
148 let r_cfg = reader.properties();
149 let w_cfg = writer.properties();
151 // before direct operations on file descriptors ensure that all source and sink buffers are empty
152 let mut flush = || -> crate::io::Result<u64> {
153 let bytes = reader.drain_to(writer, u64::MAX)?;
154 // BufWriter buffered bytes have already been accounted for in earlier write() calls
159 let mut written = 0u64;
161 if let (CopyParams(input_meta, Some(readfd)), CopyParams(output_meta, Some(writefd))) =
165 let max_write = reader.min_limit();
167 if input_meta.copy_file_range_candidate() && output_meta.copy_file_range_candidate() {
168 let result = copy_regular_files(readfd, writefd, max_write);
171 CopyResult::Ended(Ok(bytes_copied)) => return Ok(bytes_copied + written),
172 CopyResult::Ended(err) => return err,
173 CopyResult::Fallback(bytes) => written += bytes,
177 // on modern kernels sendfile can copy from any mmapable type (some but not all regular files and block devices)
178 // to any writable file descriptor. On older kernels the writer side can only be a socket.
179 // So we just try and fallback if needed.
180 // If current file offsets + write sizes overflow it may also fail, we do not try to fix that and instead
181 // fall back to the generic copy loop.
182 if input_meta.potential_sendfile_source() {
183 let result = sendfile_splice(SpliceMode::Sendfile, readfd, writefd, max_write);
186 CopyResult::Ended(Ok(bytes_copied)) => return Ok(bytes_copied + written),
187 CopyResult::Ended(err) => return err,
188 CopyResult::Fallback(bytes) => written += bytes,
192 if input_meta.maybe_fifo() || output_meta.maybe_fifo() {
193 let result = sendfile_splice(SpliceMode::Splice, readfd, writefd, max_write);
196 CopyResult::Ended(Ok(bytes_copied)) => return Ok(bytes_copied + written),
197 CopyResult::Ended(err) => return err,
198 CopyResult::Fallback(0) => { /* use the fallback below */ }
199 CopyResult::Fallback(_) => {
200 unreachable!("splice should not return > 0 bytes on the fallback path")
206 // fallback if none of the more specialized syscalls wants to work with these file descriptors
207 match generic_copy(reader, writer) {
208 Ok(bytes) => Ok(bytes + written),
214 #[rustc_specialization_trait]
215 trait CopyRead: Read {
216 /// Implementations that contain buffers (i.e. `BufReader`) must transfer data from their internal
217 /// buffers into `writer` until either the buffers are emptied or `limit` bytes have been
218 /// transferred, whichever occurs sooner.
219 /// If nested buffers are present the outer buffers must be drained first.
221 /// This is necessary to directly bypass the wrapper types while preserving the data order
222 /// when operating directly on the underlying file descriptors.
223 fn drain_to<W: Write>(&mut self, _writer: &mut W, _limit: u64) -> Result<u64> {
227 /// The minimum of the limit of all `Take<_>` wrappers, `u64::MAX` otherwise.
228 /// This method does not account for data `BufReader` buffers and would underreport
229 /// the limit of a `Take<BufReader<Take<_>>>` type. Thus its result is only valid
230 /// after draining the buffers via `drain_to`.
231 fn min_limit(&self) -> u64 {
235 /// Extracts the file descriptor and hints/metadata, delegating through wrappers if necessary.
236 fn properties(&self) -> CopyParams;
239 #[rustc_specialization_trait]
240 trait CopyWrite: Write {
241 /// Extracts the file descriptor and hints/metadata, delegating through wrappers if necessary.
242 fn properties(&self) -> CopyParams;
245 impl<T> CopyRead for &mut T
249 fn drain_to<W: Write>(&mut self, writer: &mut W, limit: u64) -> Result<u64> {
250 (**self).drain_to(writer, limit)
253 fn min_limit(&self) -> u64 {
257 fn properties(&self) -> CopyParams {
258 (**self).properties()
262 impl<T> CopyWrite for &mut T
266 fn properties(&self) -> CopyParams {
267 (**self).properties()
271 impl CopyRead for File {
272 fn properties(&self) -> CopyParams {
273 CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
277 impl CopyRead for &File {
278 fn properties(&self) -> CopyParams {
279 CopyParams(fd_to_meta(*self), Some(self.as_raw_fd()))
283 impl CopyWrite for File {
284 fn properties(&self) -> CopyParams {
285 CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
289 impl CopyWrite for &File {
290 fn properties(&self) -> CopyParams {
291 CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
295 impl CopyRead for TcpStream {
296 fn properties(&self) -> CopyParams {
297 // avoid the stat syscall since we can be fairly sure it's a socket
298 CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
302 impl CopyRead for &TcpStream {
303 fn properties(&self) -> CopyParams {
304 // avoid the stat syscall since we can be fairly sure it's a socket
305 CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
309 impl CopyWrite for TcpStream {
310 fn properties(&self) -> CopyParams {
311 // avoid the stat syscall since we can be fairly sure it's a socket
312 CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
316 impl CopyWrite for &TcpStream {
317 fn properties(&self) -> CopyParams {
318 // avoid the stat syscall since we can be fairly sure it's a socket
319 CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
323 impl CopyWrite for ChildStdin {
324 fn properties(&self) -> CopyParams {
325 CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
329 impl CopyRead for ChildStdout {
330 fn properties(&self) -> CopyParams {
331 CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
335 impl CopyRead for ChildStderr {
336 fn properties(&self) -> CopyParams {
337 CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
341 impl CopyRead for StdinLock<'_> {
342 fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
343 let buf_reader = self.as_mut_buf();
344 let buf = buf_reader.buffer();
345 let buf = &buf[0..min(buf.len(), outer_limit.try_into().unwrap_or(usize::MAX))];
346 let bytes_drained = buf.len();
347 writer.write_all(buf)?;
348 buf_reader.consume(bytes_drained);
350 Ok(bytes_drained as u64)
353 fn properties(&self) -> CopyParams {
354 CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
358 impl CopyWrite for StdoutLock<'_> {
359 fn properties(&self) -> CopyParams {
360 CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
364 impl CopyWrite for StderrLock<'_> {
365 fn properties(&self) -> CopyParams {
366 CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
370 impl<T: CopyRead> CopyRead for Take<T> {
371 fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
372 let local_limit = self.limit();
373 let combined_limit = min(outer_limit, local_limit);
374 let bytes_drained = self.get_mut().drain_to(writer, combined_limit)?;
375 // update limit since read() was bypassed
376 self.set_limit(local_limit - bytes_drained);
381 fn min_limit(&self) -> u64 {
382 min(Take::limit(self), self.get_ref().min_limit())
385 fn properties(&self) -> CopyParams {
386 self.get_ref().properties()
390 impl<T: CopyRead> CopyRead for BufReader<T> {
391 fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
392 let buf = self.buffer();
393 let buf = &buf[0..min(buf.len(), outer_limit.try_into().unwrap_or(usize::MAX))];
394 let bytes = buf.len();
395 writer.write_all(buf)?;
398 let remaining = outer_limit - bytes as u64;
400 // in case of nested bufreaders we also need to drain the ones closer to the source
401 let inner_bytes = self.get_mut().drain_to(writer, remaining)?;
403 Ok(bytes as u64 + inner_bytes)
406 fn min_limit(&self) -> u64 {
407 self.get_ref().min_limit()
410 fn properties(&self) -> CopyParams {
411 self.get_ref().properties()
415 impl<T: CopyWrite> CopyWrite for BufWriter<T> {
416 fn properties(&self) -> CopyParams {
417 self.get_ref().properties()
421 fn fd_to_meta<T: AsRawFd>(fd: &T) -> FdMeta {
422 let fd = fd.as_raw_fd();
423 let file: ManuallyDrop<File> = ManuallyDrop::new(unsafe { File::from_raw_fd(fd) });
424 match file.metadata() {
425 Ok(meta) => FdMeta::Metadata(meta),
426 Err(_) => FdMeta::NoneObtained,
430 pub(super) enum CopyResult {
435 /// linux-specific implementation that will attempt to use copy_file_range for copy offloading
436 /// as the name says, it only works on regular files
438 /// Callers must handle fallback to a generic copy loop.
439 /// `Fallback` may indicate non-zero number of bytes already written
440 /// if one of the files' cursor +`max_len` would exceed u64::MAX (`EOVERFLOW`).
441 pub(super) fn copy_regular_files(reader: RawFd, writer: RawFd, max_len: u64) -> CopyResult {
444 // Kernel prior to 4.5 don't have copy_file_range
445 // We store the availability in a global to avoid unnecessary syscalls
446 static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
451 off_in: *mut libc::loff_t,
453 off_out: *mut libc::loff_t,
459 let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
460 let mut written = 0u64;
461 while written < max_len {
462 let copy_result = if has_copy_file_range {
463 let bytes_to_copy = cmp::min(max_len - written, usize::MAX as u64);
464 // cap to 1GB chunks in case u64::MAX is passed as max_len and the file has a non-zero seek position
465 // this allows us to copy large chunks without hitting EOVERFLOW,
466 // unless someone sets a file offset close to u64::MAX - 1GB, in which case a fallback would be required
467 let bytes_to_copy = cmp::min(bytes_to_copy as usize, 0x4000_0000usize);
468 let copy_result = unsafe {
469 // We actually don't have to adjust the offsets,
470 // because copy_file_range adjusts the file offset automatically
480 if let Err(ref copy_err) = copy_result {
481 match copy_err.raw_os_error() {
482 Some(libc::ENOSYS | libc::EPERM | libc::EOPNOTSUPP) => {
483 HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
490 Err(Error::from_raw_os_error(libc::ENOSYS))
493 Ok(0) if written == 0 => {
494 // fallback to work around several kernel bugs where copy_file_range will fail to
495 // copy any bytes and return 0 instead of an error if
496 // - reading virtual files from the proc filesystem which appear to have 0 size
497 // but are not empty. noted in coreutils to affect kernels at least up to 5.6.19.
498 // - copying from an overlay filesystem in docker. reported to occur on fedora 32.
499 return CopyResult::Fallback(0);
501 Ok(0) => return CopyResult::Ended(Ok(written)), // reached EOF
502 Ok(ret) => written += ret as u64,
504 return match err.raw_os_error() {
505 // when file offset + max_length > u64::MAX
506 Some(libc::EOVERFLOW) => CopyResult::Fallback(written),
508 libc::ENOSYS | libc::EXDEV | libc::EINVAL | libc::EPERM | libc::EOPNOTSUPP,
510 // Try fallback io::copy if either:
511 // - Kernel version is < 4.5 (ENOSYS)
512 // - Files are mounted on different fs (EXDEV)
513 // - copy_file_range is broken in various ways on RHEL/CentOS 7 (EOPNOTSUPP)
514 // - copy_file_range is disallowed, for example by seccomp (EPERM)
515 // - copy_file_range cannot be used with pipes or device nodes (EINVAL)
516 assert_eq!(written, 0);
517 CopyResult::Fallback(0)
519 _ => CopyResult::Ended(Err(err)),
524 CopyResult::Ended(Ok(written))
533 /// performs splice or sendfile between file descriptors
534 /// Does _not_ fall back to a generic copy loop.
535 fn sendfile_splice(mode: SpliceMode, reader: RawFd, writer: RawFd, len: u64) -> CopyResult {
536 static HAS_SENDFILE: AtomicBool = AtomicBool::new(true);
537 static HAS_SPLICE: AtomicBool = AtomicBool::new(true);
542 src_offset: *const i64,
544 dst_offset: *const i64,
551 SpliceMode::Sendfile if !HAS_SENDFILE.load(Ordering::Relaxed) => {
552 return CopyResult::Fallback(0);
554 SpliceMode::Splice if !HAS_SPLICE.load(Ordering::Relaxed) => {
555 return CopyResult::Fallback(0);
560 let mut written = 0u64;
561 while written < len {
562 // according to its manpage that's the maximum size sendfile() will copy per invocation
563 let chunk_size = crate::cmp::min(len - written, 0x7ffff000_u64) as usize;
565 let result = match mode {
566 SpliceMode::Sendfile => {
567 cvt(unsafe { libc::sendfile(writer, reader, ptr::null_mut(), chunk_size) })
569 SpliceMode::Splice => cvt(unsafe {
570 splice(reader, ptr::null_mut(), writer, ptr::null_mut(), chunk_size, 0)
575 Ok(0) => break, // EOF
576 Ok(ret) => written += ret as u64,
578 return match err.raw_os_error() {
579 Some(libc::ENOSYS | libc::EPERM) => {
580 // syscall not supported (ENOSYS)
581 // syscall is disallowed, e.g. by seccomp (EPERM)
583 SpliceMode::Sendfile => HAS_SENDFILE.store(false, Ordering::Relaxed),
584 SpliceMode::Splice => HAS_SPLICE.store(false, Ordering::Relaxed),
586 assert_eq!(written, 0);
587 CopyResult::Fallback(0)
589 Some(libc::EINVAL) => {
590 // splice/sendfile do not support this particular file descriptor (EINVAL)
591 assert_eq!(written, 0);
592 CopyResult::Fallback(0)
594 Some(os_err) if mode == SpliceMode::Sendfile && os_err == libc::EOVERFLOW => {
595 CopyResult::Fallback(written)
597 _ => CopyResult::Ended(Err(err)),
602 CopyResult::Ended(Ok(written))