Skip to content

Commit cb1c156

Browse files
authored
[libc++] Use copy_file_range for fs::copy (llvm#109211)
This optimizes `std::filesystem::copy_file` to use the `copy_file_range` syscall (Linux and FreeBSD) when available. It allows for reflinks on filesystems such as btrfs, zfs and xfs, and server-side copy for network filesystems such as NFS.
1 parent 15f30e7 commit cb1c156

File tree

2 files changed

+192
-33
lines changed

2 files changed

+192
-33
lines changed

libcxx/src/filesystem/operations.cpp

Lines changed: 139 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <filesystem>
1616
#include <iterator>
1717
#include <string_view>
18+
#include <system_error>
1819
#include <type_traits>
1920
#include <vector>
2021

@@ -32,22 +33,35 @@
3233
# include <dirent.h>
3334
# include <sys/stat.h>
3435
# include <sys/statvfs.h>
36+
# include <sys/types.h>
3537
# include <unistd.h>
3638
#endif
3739
#include <fcntl.h> /* values for fchmodat */
3840
#include <time.h>
3941

42+
// since Linux 4.5 and FreeBSD 13, but the Linux libc wrapper is only provided by glibc and musl
43+
#if (defined(__linux__) && (defined(__GLIBC__) || _LIBCPP_HAS_MUSL_LIBC)) || defined(__FreeBSD__)
44+
# define _LIBCPP_FILESYSTEM_USE_COPY_FILE_RANGE
45+
#endif
4046
#if __has_include(<sys/sendfile.h>)
4147
# include <sys/sendfile.h>
4248
# define _LIBCPP_FILESYSTEM_USE_SENDFILE
4349
#elif defined(__APPLE__) || __has_include(<copyfile.h>)
4450
# include <copyfile.h>
4551
# define _LIBCPP_FILESYSTEM_USE_COPYFILE
4652
#else
47-
# include <fstream>
4853
# define _LIBCPP_FILESYSTEM_USE_FSTREAM
4954
#endif
5055

56+
// sendfile and copy_file_range need to fall back
57+
// to the fstream implementation for special files
58+
#if (defined(_LIBCPP_FILESYSTEM_USE_SENDFILE) || defined(_LIBCPP_FILESYSTEM_USE_COPY_FILE_RANGE) || \
59+
defined(_LIBCPP_FILESYSTEM_USE_FSTREAM)) && \
60+
_LIBCPP_HAS_LOCALIZATION
61+
# include <fstream>
62+
# define _LIBCPP_FILESYSTEM_NEED_FSTREAM
63+
#endif
64+
5165
#if defined(__ELF__) && defined(_LIBCPP_LINK_RT_LIB)
5266
# pragma comment(lib, "rt")
5367
#endif
@@ -178,9 +192,83 @@ void __copy(const path& from, const path& to, copy_options options, error_code*
178192
namespace detail {
179193
namespace {
180194

195+
#if defined(_LIBCPP_FILESYSTEM_NEED_FSTREAM)
196+
bool copy_file_impl_fstream(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
197+
ifstream in;
198+
in.__open(read_fd.fd, ios::binary);
199+
if (!in.is_open()) {
200+
// This assumes that __open didn't reset the error code.
201+
ec = capture_errno();
202+
return false;
203+
}
204+
read_fd.fd = -1;
205+
ofstream out;
206+
out.__open(write_fd.fd, ios::binary);
207+
if (!out.is_open()) {
208+
ec = capture_errno();
209+
return false;
210+
}
211+
write_fd.fd = -1;
212+
213+
if (in.good() && out.good()) {
214+
using InIt = istreambuf_iterator<char>;
215+
using OutIt = ostreambuf_iterator<char>;
216+
InIt bin(in);
217+
InIt ein;
218+
OutIt bout(out);
219+
copy(bin, ein, bout);
220+
}
221+
if (out.fail() || in.fail()) {
222+
ec = make_error_code(errc::io_error);
223+
return false;
224+
}
225+
226+
ec.clear();
227+
return true;
228+
}
229+
#endif
230+
231+
#if defined(_LIBCPP_FILESYSTEM_USE_COPY_FILE_RANGE)
232+
bool copy_file_impl_copy_file_range(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
233+
size_t count = read_fd.get_stat().st_size;
234+
// a zero-length file is either empty, or not copyable by this syscall
235+
// return early to avoid the syscall cost
236+
if (count == 0) {
237+
ec = {EINVAL, generic_category()};
238+
return false;
239+
}
240+
// do not modify the fd positions as copy_file_impl_sendfile may be called after a partial copy
241+
off_t off_in = 0;
242+
off_t off_out = 0;
243+
do {
244+
ssize_t res;
245+
246+
if ((res = ::copy_file_range(read_fd.fd, &off_in, write_fd.fd, &off_out, count, 0)) == -1) {
247+
ec = capture_errno();
248+
return false;
249+
}
250+
count -= res;
251+
} while (count > 0);
252+
253+
ec.clear();
254+
255+
return true;
256+
}
257+
#endif
258+
181259
#if defined(_LIBCPP_FILESYSTEM_USE_SENDFILE)
182-
bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
260+
bool copy_file_impl_sendfile(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
183261
size_t count = read_fd.get_stat().st_size;
262+
// a zero-length file is either empty, or not copyable by this syscall
263+
// return early to avoid the syscall cost
264+
// however, we can't afford this luxury in the no-locale build,
265+
// as we can't utilize the fstream impl to copy empty files
266+
# if _LIBCPP_HAS_LOCALIZATION
267+
if (count == 0) {
268+
ec = {EINVAL, generic_category()};
269+
return false;
270+
}
271+
# endif
184272
do {
185273
ssize_t res;
186274
if ((res = ::sendfile(write_fd.fd, read_fd.fd, nullptr, count)) == -1) {
@@ -194,6 +282,54 @@ bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_cod
194282

195283
return true;
196284
}
285+
#endif
286+
287+
#if defined(_LIBCPP_FILESYSTEM_USE_COPY_FILE_RANGE) || defined(_LIBCPP_FILESYSTEM_USE_SENDFILE)
288+
// If we have copy_file_range or sendfile, try both in succession (if available).
289+
// If both fail, fall back to using fstream.
290+
bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
291+
# if defined(_LIBCPP_FILESYSTEM_USE_COPY_FILE_RANGE)
292+
if (copy_file_impl_copy_file_range(read_fd, write_fd, ec)) {
293+
return true;
294+
}
295+
// EINVAL: src and dst are the same file (this is not cheaply
296+
// detectable from userspace)
297+
// EINVAL: copy_file_range is unsupported for this file type by the
298+
// underlying filesystem
299+
// ENOTSUP: undocumented, can arise with old kernels and NFS
300+
// EOPNOTSUPP: filesystem does not implement copy_file_range
301+
// ETXTBSY: src or dst is an active swapfile (nonsensical, but allowed
302+
// with normal copying)
303+
// EXDEV: src and dst are on different filesystems that do not support
304+
// cross-fs copy_file_range
305+
// ENOENT: undocumented, can arise with CIFS
306+
// ENOSYS: unsupported by kernel or blocked by seccomp
307+
if (ec.value() != EINVAL && ec.value() != ENOTSUP && ec.value() != EOPNOTSUPP && ec.value() != ETXTBSY &&
308+
ec.value() != EXDEV && ec.value() != ENOENT && ec.value() != ENOSYS) {
309+
return false;
310+
}
311+
ec.clear();
312+
# endif
313+
314+
# if defined(_LIBCPP_FILESYSTEM_USE_SENDFILE)
315+
if (copy_file_impl_sendfile(read_fd, write_fd, ec)) {
316+
return true;
317+
}
318+
// EINVAL: unsupported file type
319+
if (ec.value() != EINVAL) {
320+
return false;
321+
}
322+
ec.clear();
323+
# endif
324+
325+
# if defined(_LIBCPP_FILESYSTEM_NEED_FSTREAM)
326+
return copy_file_impl_fstream(read_fd, write_fd, ec);
327+
# else
328+
// since iostreams are unavailable in the no-locale build, just fail after a failed sendfile
329+
ec.assign(EINVAL, std::system_category());
330+
return false;
331+
# endif
332+
}
197333
#elif defined(_LIBCPP_FILESYSTEM_USE_COPYFILE)
198334
bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
199335
struct CopyFileState {
@@ -217,37 +353,7 @@ bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_cod
217353
}
218354
#elif defined(_LIBCPP_FILESYSTEM_USE_FSTREAM)
219355
bool copy_file_impl(FileDescriptor& read_fd, FileDescriptor& write_fd, error_code& ec) {
220-
ifstream in;
221-
in.__open(read_fd.fd, ios::binary);
222-
if (!in.is_open()) {
223-
// This assumes that __open didn't reset the error code.
224-
ec = capture_errno();
225-
return false;
226-
}
227-
read_fd.fd = -1;
228-
ofstream out;
229-
out.__open(write_fd.fd, ios::binary);
230-
if (!out.is_open()) {
231-
ec = capture_errno();
232-
return false;
233-
}
234-
write_fd.fd = -1;
235-
236-
if (in.good() && out.good()) {
237-
using InIt = istreambuf_iterator<char>;
238-
using OutIt = ostreambuf_iterator<char>;
239-
InIt bin(in);
240-
InIt ein;
241-
OutIt bout(out);
242-
copy(bin, ein, bout);
243-
}
244-
if (out.fail() || in.fail()) {
245-
ec = make_error_code(errc::io_error);
246-
return false;
247-
}
248-
249-
ec.clear();
250-
return true;
356+
return copy_file_impl_fstream(read_fd, write_fd, ec);
251357
}
252358
#else
253359
# error "Unknown implementation for copy_file_impl"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// UNSUPPORTED: c++03, c++11, c++14
10+
// REQUIRES: linux
11+
// UNSUPPORTED: no-filesystem
12+
// XFAIL: no-localization
13+
// UNSUPPORTED: availability-filesystem-missing
14+
15+
// <filesystem>
16+
17+
// bool copy_file(const path& from, const path& to);
18+
// bool copy_file(const path& from, const path& to, error_code& ec) noexcept;
19+
// bool copy_file(const path& from, const path& to, copy_options options);
20+
// bool copy_file(const path& from, const path& to, copy_options options,
21+
// error_code& ec) noexcept;
22+
23+
#include <cassert>
24+
#include <filesystem>
25+
#include <system_error>
26+
27+
#include "test_macros.h"
28+
#include "filesystem_test_helper.h"
29+
30+
namespace fs = std::filesystem;
31+
32+
// Linux has various virtual filesystems such as /proc and /sys
33+
// where files may have no length (st_size == 0), but still contain data.
34+
// This is because the to-be-read data is usually generated ad-hoc by the reading syscall
35+
// These files can not be copied with kernel-side copies like copy_file_range or sendfile,
36+
// and must instead be copied via a traditional userspace read + write loop.
37+
int main(int, char** argv) {
38+
const fs::path procfile{"/proc/self/comm"};
39+
assert(file_size(procfile) == 0);
40+
41+
scoped_test_env env;
42+
std::error_code ec = GetTestEC();
43+
44+
const fs::path dest = env.make_env_path("dest");
45+
46+
assert(copy_file(procfile, dest, ec));
47+
assert(!ec);
48+
49+
// /proc/self/comm contains the filename of the executable, plus a null terminator
50+
assert(file_size(dest) == fs::path(argv[0]).filename().string().size() + 1);
51+
52+
return 0;
53+
}

0 commit comments

Comments
 (0)