aboutsummaryrefslogtreecommitdiff
path: root/lib/std
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2020-10-08 20:14:47 -0400
committerGitHub <noreply@github.com>2020-10-08 20:14:47 -0400
commit76a195473dac059a842fed2a6ba581ca99947d2b (patch)
treea70b527370932d85aaa00199b99b6738ded73a8b /lib/std
parentb02341d6f58e0b8a87fc2ab589dcfd85e5dc96cd (diff)
parent03762da2af8753ecf7f4bc2005dd00d570c51ae7 (diff)
downloadzig-76a195473dac059a842fed2a6ba581ca99947d2b.tar.gz
zig-76a195473dac059a842fed2a6ba581ca99947d2b.zip
Merge pull request #6516 from LemonBoy/fastfilecopy
std: Make file copy ops use zero-copy mechanisms
Diffstat (limited to 'lib/std')
-rw-r--r--lib/std/c/darwin.zig8
-rw-r--r--lib/std/fs.zig49
-rw-r--r--lib/std/os.zig31
3 files changed, 76 insertions, 12 deletions
diff --git a/lib/std/c/darwin.zig b/lib/std/c/darwin.zig
index 976690d6b7..a3482c73f6 100644
--- a/lib/std/c/darwin.zig
+++ b/lib/std/c/darwin.zig
@@ -18,6 +18,14 @@ pub extern "c" fn _dyld_get_image_header(image_index: u32) ?*mach_header;
pub extern "c" fn _dyld_get_image_vmaddr_slide(image_index: u32) usize;
pub extern "c" fn _dyld_get_image_name(image_index: u32) [*:0]const u8;
+pub const COPYFILE_ACL = 1 << 0;
+pub const COPYFILE_STAT = 1 << 1;
+pub const COPYFILE_XATTR = 1 << 2;
+pub const COPYFILE_DATA = 1 << 3;
+
+pub const copyfile_state_t = *@Type(.Opaque);
+pub extern "c" fn fcopyfile(from: fd_t, to: fd_t, state: ?copyfile_state_t, flags: u32) c_int;
+
pub extern "c" fn @"realpath$DARWIN_EXTSN"(noalias file_name: [*:0]const u8, noalias resolved_name: [*]u8) ?[*:0]u8;
pub extern "c" fn __getdirentries64(fd: c_int, buf_ptr: [*]u8, buf_len: usize, basep: *i64) isize;
diff --git a/lib/std/fs.zig b/lib/std/fs.zig
index 22d243612a..5e440ec900 100644
--- a/lib/std/fs.zig
+++ b/lib/std/fs.zig
@@ -1823,7 +1823,7 @@ pub const Dir = struct {
var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode });
defer atomic_file.deinit();
- try atomic_file.file.writeFileAll(in_file, .{ .in_len = size });
+ try copy_file(in_file.handle, atomic_file.file.handle);
return atomic_file.finish();
}
@@ -2271,6 +2271,53 @@ pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 {
return allocator.dupe(u8, try os.realpath(pathname, &buf));
}
+const CopyFileError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError;
+
+// Transfer all the data between two file descriptors in the most efficient way.
+// The copy starts at offset 0, the initial offsets are preserved.
+// No metadata is transferred over.
+fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t) CopyFileError!void {
+ if (comptime std.Target.current.isDarwin()) {
+ const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA);
+ switch (os.errno(rc)) {
+ 0 => return,
+ os.EINVAL => unreachable,
+ os.ENOMEM => return error.SystemResources,
+ // The source file is not a directory, symbolic link, or regular file.
+ // Try with the fallback path before giving up.
+ os.ENOTSUP => {},
+ else => |err| return os.unexpectedErrno(err),
+ }
+ }
+
+ if (std.Target.current.os.tag == .linux) {
+ // Try copy_file_range first as that works at the FS level and is the
+ // most efficient method (if available).
+ var offset: u64 = 0;
+ cfr_loop: while (true) {
+ // The kernel checks the u64 value `offset+count` for overflow, use
+ // a 32 bit value so that the syscall won't return EINVAL except for
+ // impossibly large files (> 2^64-1 - 2^32-1).
+ const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0);
+ // Terminate when no data was copied
+ if (amt == 0) break :cfr_loop;
+ offset += amt;
+ }
+ return;
+ }
+
+ // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the
+ // fallback code will copy the contents chunk by chunk.
+ const empty_iovec = [0]os.iovec_const{};
+ var offset: u64 = 0;
+ sendfile_loop: while (true) {
+ const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0);
+ // Terminate when no data was copied
+ if (amt == 0) break :sendfile_loop;
+ offset += amt;
+ }
+}
+
test "" {
if (builtin.os.tag != .wasi) {
_ = makeDirAbsolute;
diff --git a/lib/std/os.zig b/lib/std/os.zig
index c89f122c63..9f6c012090 100644
--- a/lib/std/os.zig
+++ b/lib/std/os.zig
@@ -4945,6 +4945,9 @@ pub fn sendfile(
pub const CopyFileRangeError = error{
FileTooBig,
InputOutput,
+ /// `fd_in` is not open for reading; or `fd_out` is not open for writing;
+ /// or the `O_APPEND` flag is set for `fd_out`.
+ FilesOpenedWithWrongFlags,
IsDir,
OutOfMemory,
NoSpaceLeft,
@@ -4953,6 +4956,11 @@ pub const CopyFileRangeError = error{
FileBusy,
} || PReadError || PWriteError || UnexpectedError;
+var has_copy_file_range_syscall = init: {
+ const kernel_has_syscall = std.Target.current.os.isAtLeast(.linux, .{ .major = 4, .minor = 5 }) orelse true;
+ break :init std.atomic.Int(bool).init(kernel_has_syscall);
+};
+
/// Transfer data between file descriptors at specified offsets.
/// Returns the number of bytes written, which can less than requested.
///
@@ -4981,22 +4989,18 @@ pub const CopyFileRangeError = error{
pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len: usize, flags: u32) CopyFileRangeError!usize {
const use_c = std.c.versionCheck(.{ .major = 2, .minor = 27, .patch = 0 }).ok;
- // TODO support for other systems than linux
- const try_syscall = comptime std.Target.current.os.isAtLeast(.linux, .{ .major = 4, .minor = 5 }) != false;
-
- if (use_c or try_syscall) {
+ if (std.Target.current.os.tag == .linux and
+ (use_c or has_copy_file_range_syscall.get()))
+ {
const sys = if (use_c) std.c else linux;
var off_in_copy = @bitCast(i64, off_in);
var off_out_copy = @bitCast(i64, off_out);
const rc = sys.copy_file_range(fd_in, &off_in_copy, fd_out, &off_out_copy, len, flags);
-
- // TODO avoid wasting a syscall every time if kernel is too old and returns ENOSYS https://github.com/ziglang/zig/issues/1018
-
switch (sys.getErrno(rc)) {
0 => return @intCast(usize, rc),
- EBADF => unreachable,
+ EBADF => return error.FilesOpenedWithWrongFlags,
EFBIG => return error.FileTooBig,
EIO => return error.InputOutput,
EISDIR => return error.IsDir,
@@ -5005,9 +5009,14 @@ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len
EOVERFLOW => return error.Unseekable,
EPERM => return error.PermissionDenied,
ETXTBSY => return error.FileBusy,
- EINVAL => {}, // these may not be regular files, try fallback
- EXDEV => {}, // support for cross-filesystem copy added in Linux 5.3, use fallback
- ENOSYS => {}, // syscall added in Linux 4.5, use fallback
+ // these may not be regular files, try fallback
+ EINVAL => {},
+ // support for cross-filesystem copy added in Linux 5.3, use fallback
+ EXDEV => {},
+ // syscall added in Linux 4.5, use fallback
+ ENOSYS => {
+ has_copy_file_range_syscall.set(false);
+ },
else => |err| return unexpectedErrno(err),
}
}