]> git.lizzy.rs Git - rust.git/blob - library/std/src/sys/unix/kernel_copy.rs
Merge commit '5988bbd24aa87732bfa1d111ba00bcdaa22c481a' into sync_cg_clif-2020-11-27
[rust.git] / library / std / src / sys / unix / kernel_copy.rs
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)`.
3 //!
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.
6 //!
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.
10 //!
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.
13 //!
14 //! `Copier` uses the specialization traits to unpack the underlying file descriptors and
15 //! additional prerequisites and constraints imposed by the wrapper types.
16 //!
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
24 //! does.
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.
27 //!
28 //! Advantages of using these syscalls:
29 //!
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.
37 //!
38 //! Drawbacks:
39 //!
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.
45 //! * complexity
46
47 use crate::cmp::min;
48 use crate::convert::TryInto;
49 use crate::fs::{File, Metadata};
50 use crate::io::copy::generic_copy;
51 use crate::io::{
52     BufRead, BufReader, BufWriter, Error, Read, Result, StderrLock, StdinLock, StdoutLock, Take,
53     Write,
54 };
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};
60 use crate::ptr;
61 use crate::sync::atomic::{AtomicBool, Ordering};
62 use crate::sys::cvt;
63
64 #[cfg(test)]
65 mod tests;
66
67 pub(crate) fn copy_spec<R: Read + ?Sized, W: Write + ?Sized>(
68     read: &mut R,
69     write: &mut W,
70 ) -> Result<u64> {
71     let copier = Copier { read, write };
72     SpecCopy::copy(copier)
73 }
74
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
77 ///
78 /// The methods on this type only provide hints, due to `AsRawFd` and `FromRawFd` the inferred
79 /// type may be wrong.
80 enum FdMeta {
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)
83     Metadata(Metadata),
84     Socket,
85     Pipe,
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
88     /// (writer side)
89     NoneObtained,
90 }
91
92 impl FdMeta {
93     fn maybe_fifo(&self) -> bool {
94         match self {
95             FdMeta::Metadata(meta) => meta.file_type().is_fifo(),
96             FdMeta::Socket => false,
97             FdMeta::Pipe => true,
98             FdMeta::NoneObtained => true,
99         }
100     }
101
102     fn potential_sendfile_source(&self) -> bool {
103         match self {
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() =>
110             {
111                 true
112             }
113             _ => false,
114         }
115     }
116
117     fn copy_file_range_candidate(&self) -> bool {
118         match self {
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,
123             _ => false,
124         }
125     }
126 }
127
128 struct CopyParams(FdMeta, Option<RawFd>);
129
130 struct Copier<'a, 'b, R: Read + ?Sized, W: Write + ?Sized> {
131     read: &'a mut R,
132     write: &'b mut W,
133 }
134
135 trait SpecCopy {
136     fn copy(self) -> Result<u64>;
137 }
138
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)
142     }
143 }
144
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();
150
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
155             writer.flush()?;
156             Ok(bytes)
157         };
158
159         let mut written = 0u64;
160
161         if let (CopyParams(input_meta, Some(readfd)), CopyParams(output_meta, Some(writefd))) =
162             (r_cfg, w_cfg)
163         {
164             written += flush()?;
165             let max_write = reader.min_limit();
166
167             if input_meta.copy_file_range_candidate() && output_meta.copy_file_range_candidate() {
168                 let result = copy_regular_files(readfd, writefd, max_write);
169
170                 match result {
171                     CopyResult::Ended(Ok(bytes_copied)) => return Ok(bytes_copied + written),
172                     CopyResult::Ended(err) => return err,
173                     CopyResult::Fallback(bytes) => written += bytes,
174                 }
175             }
176
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);
184
185                 match result {
186                     CopyResult::Ended(Ok(bytes_copied)) => return Ok(bytes_copied + written),
187                     CopyResult::Ended(err) => return err,
188                     CopyResult::Fallback(bytes) => written += bytes,
189                 }
190             }
191
192             if input_meta.maybe_fifo() || output_meta.maybe_fifo() {
193                 let result = sendfile_splice(SpliceMode::Splice, readfd, writefd, max_write);
194
195                 match result {
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")
201                     }
202                 }
203             }
204         }
205
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),
209             err => err,
210         }
211     }
212 }
213
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.
220     ///
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> {
224         Ok(0)
225     }
226
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 {
232         u64::MAX
233     }
234
235     /// Extracts the file descriptor and hints/metadata, delegating through wrappers if necessary.
236     fn properties(&self) -> CopyParams;
237 }
238
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;
243 }
244
245 impl<T> CopyRead for &mut T
246 where
247     T: CopyRead,
248 {
249     fn drain_to<W: Write>(&mut self, writer: &mut W, limit: u64) -> Result<u64> {
250         (**self).drain_to(writer, limit)
251     }
252
253     fn min_limit(&self) -> u64 {
254         (**self).min_limit()
255     }
256
257     fn properties(&self) -> CopyParams {
258         (**self).properties()
259     }
260 }
261
262 impl<T> CopyWrite for &mut T
263 where
264     T: CopyWrite,
265 {
266     fn properties(&self) -> CopyParams {
267         (**self).properties()
268     }
269 }
270
271 impl CopyRead for File {
272     fn properties(&self) -> CopyParams {
273         CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
274     }
275 }
276
277 impl CopyRead for &File {
278     fn properties(&self) -> CopyParams {
279         CopyParams(fd_to_meta(*self), Some(self.as_raw_fd()))
280     }
281 }
282
283 impl CopyWrite for File {
284     fn properties(&self) -> CopyParams {
285         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
286     }
287 }
288
289 impl CopyWrite for &File {
290     fn properties(&self) -> CopyParams {
291         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
292     }
293 }
294
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()))
299     }
300 }
301
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()))
306     }
307 }
308
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()))
313     }
314 }
315
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()))
320     }
321 }
322
323 impl CopyWrite for ChildStdin {
324     fn properties(&self) -> CopyParams {
325         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
326     }
327 }
328
329 impl CopyRead for ChildStdout {
330     fn properties(&self) -> CopyParams {
331         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
332     }
333 }
334
335 impl CopyRead for ChildStderr {
336     fn properties(&self) -> CopyParams {
337         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
338     }
339 }
340
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);
349
350         Ok(bytes_drained as u64)
351     }
352
353     fn properties(&self) -> CopyParams {
354         CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
355     }
356 }
357
358 impl CopyWrite for StdoutLock<'_> {
359     fn properties(&self) -> CopyParams {
360         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
361     }
362 }
363
364 impl CopyWrite for StderrLock<'_> {
365     fn properties(&self) -> CopyParams {
366         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
367     }
368 }
369
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);
377
378         Ok(bytes_drained)
379     }
380
381     fn min_limit(&self) -> u64 {
382         min(Take::limit(self), self.get_ref().min_limit())
383     }
384
385     fn properties(&self) -> CopyParams {
386         self.get_ref().properties()
387     }
388 }
389
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)?;
396         self.consume(bytes);
397
398         let remaining = outer_limit - bytes as u64;
399
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)?;
402
403         Ok(bytes as u64 + inner_bytes)
404     }
405
406     fn min_limit(&self) -> u64 {
407         self.get_ref().min_limit()
408     }
409
410     fn properties(&self) -> CopyParams {
411         self.get_ref().properties()
412     }
413 }
414
415 impl<T: CopyWrite> CopyWrite for BufWriter<T> {
416     fn properties(&self) -> CopyParams {
417         self.get_ref().properties()
418     }
419 }
420
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,
427     }
428 }
429
430 pub(super) enum CopyResult {
431     Ended(Result<u64>),
432     Fallback(u64),
433 }
434
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
437 ///
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 {
442     use crate::cmp;
443
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);
447
448     syscall! {
449         fn copy_file_range(
450             fd_in: libc::c_int,
451             off_in: *mut libc::loff_t,
452             fd_out: libc::c_int,
453             off_out: *mut libc::loff_t,
454             len: libc::size_t,
455             flags: libc::c_uint
456         ) -> libc::ssize_t
457     }
458
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
471                 cvt(copy_file_range(
472                     reader,
473                     ptr::null_mut(),
474                     writer,
475                     ptr::null_mut(),
476                     bytes_to_copy,
477                     0,
478                 ))
479             };
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);
484                     }
485                     _ => {}
486                 }
487             }
488             copy_result
489         } else {
490             Err(Error::from_raw_os_error(libc::ENOSYS))
491         };
492         match copy_result {
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);
500             }
501             Ok(0) => return CopyResult::Ended(Ok(written)), // reached EOF
502             Ok(ret) => written += ret as u64,
503             Err(err) => {
504                 return match err.raw_os_error() {
505                     // when file offset + max_length > u64::MAX
506                     Some(libc::EOVERFLOW) => CopyResult::Fallback(written),
507                     Some(
508                         libc::ENOSYS | libc::EXDEV | libc::EINVAL | libc::EPERM | libc::EOPNOTSUPP,
509                     ) => {
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)
518                     }
519                     _ => CopyResult::Ended(Err(err)),
520                 };
521             }
522         }
523     }
524     CopyResult::Ended(Ok(written))
525 }
526
527 #[derive(PartialEq)]
528 enum SpliceMode {
529     Sendfile,
530     Splice,
531 }
532
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);
538
539     syscall! {
540         fn splice(
541             srcfd: libc::c_int,
542             src_offset: *const i64,
543             dstfd: libc::c_int,
544             dst_offset: *const i64,
545             len: libc::size_t,
546             flags: libc::c_int
547         ) -> libc::ssize_t
548     }
549
550     match mode {
551         SpliceMode::Sendfile if !HAS_SENDFILE.load(Ordering::Relaxed) => {
552             return CopyResult::Fallback(0);
553         }
554         SpliceMode::Splice if !HAS_SPLICE.load(Ordering::Relaxed) => {
555             return CopyResult::Fallback(0);
556         }
557         _ => (),
558     }
559
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;
564
565         let result = match mode {
566             SpliceMode::Sendfile => {
567                 cvt(unsafe { libc::sendfile(writer, reader, ptr::null_mut(), chunk_size) })
568             }
569             SpliceMode::Splice => cvt(unsafe {
570                 splice(reader, ptr::null_mut(), writer, ptr::null_mut(), chunk_size, 0)
571             }),
572         };
573
574         match result {
575             Ok(0) => break, // EOF
576             Ok(ret) => written += ret as u64,
577             Err(err) => {
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)
582                         match mode {
583                             SpliceMode::Sendfile => HAS_SENDFILE.store(false, Ordering::Relaxed),
584                             SpliceMode::Splice => HAS_SPLICE.store(false, Ordering::Relaxed),
585                         }
586                         assert_eq!(written, 0);
587                         CopyResult::Fallback(0)
588                     }
589                     Some(libc::EINVAL) => {
590                         // splice/sendfile do not support this particular file descriptor (EINVAL)
591                         assert_eq!(written, 0);
592                         CopyResult::Fallback(0)
593                     }
594                     Some(os_err) if mode == SpliceMode::Sendfile && os_err == libc::EOVERFLOW => {
595                         CopyResult::Fallback(written)
596                     }
597                     _ => CopyResult::Ended(Err(err)),
598                 };
599             }
600         }
601     }
602     CopyResult::Ended(Ok(written))
603 }