-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
std: Make file copy ops use zero-copy mechanisms #6516
Changes from 2 commits
0f248e0
8b4f5f0
a419a1a
1f7ec0d
03762da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 os.copy_file(in_file.handle, atomic_file.file.handle, .{}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docs for
Did we gain a redundant fstat syscall in the case that sendfile has to be used? I know we've had this discussion before, and your position is that it doesn't matter in practice, but this is fundamental to zig's premise as a language and standard library, that it does the "optimal" interaction with the OS. I think it's an important sign of implementation quality for an strace / ProcMon session to contain only necessary syscalls. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, both |
||
return atomic_file.finish(); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4981,19 +4981,15 @@ 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() != 0)) | ||
{ | ||
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, | ||
|
@@ -5005,9 +5001,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(0); | ||
}, | ||
else => |err| return unexpectedErrno(err), | ||
} | ||
} | ||
|
@@ -5021,6 +5022,89 @@ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len | |
return pwrite(fd_out, buf[0..amt_read], off_out); | ||
} | ||
|
||
var has_copy_file_range_syscall = std.atomic.Int(u1).init(1); | ||
|
||
pub const CopyFileOptions = struct {}; | ||
|
||
pub const CopyFileError = error{ | ||
BadFileHandle, | ||
SystemResources, | ||
FileTooBig, | ||
InputOutput, | ||
IsDir, | ||
OutOfMemory, | ||
NoSpaceLeft, | ||
Unseekable, | ||
PermissionDenied, | ||
FileBusy, | ||
} || FStatError || SendFileError; | ||
|
||
/// Transfer all the data between two file descriptors in the most efficient way. | ||
/// No metadata is transferred over. | ||
pub fn copy_file(fd_in: fd_t, fd_out: fd_t, options: CopyFileOptions) CopyFileError!void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the "posix layer" API function that this represents? It looks like the only one that has such a function is Darwin, in which case this should be called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no POSIX equivalent, we can just move this into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw if you haven't seen it: #5019 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IMO we don't need a posix layer, we only need a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that creating a full blown posix compatibility layer is not a goal in and of itself. But it's an implementation detail of providing a std that does things (e.g. |
||
if (comptime std.Target.current.isDarwin()) { | ||
const rc = system.fcopyfile(fd_in, fd_out, null, system.COPYFILE_DATA); | ||
switch (errno(rc)) { | ||
0 => return, | ||
EINVAL => unreachable, | ||
ENOMEM => return error.SystemResources, | ||
// The source file was not a directory, symbolic link, or regular file. | ||
// Try with the fallback path before giving up. | ||
ENOTSUP => {}, | ||
else => |err| return 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). | ||
if (has_copy_file_range_syscall.get() != 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use the target version range to determine which state we are in:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure thing, it's enough to preload |
||
cfr_loop: while (true) { | ||
// The kernel checks `file_pos+count` for overflow, use a 32 bit | ||
// value so that the syscall won't return EINVAL except for | ||
// impossibly large files. | ||
const rc = linux.copy_file_range(fd_in, null, fd_out, null, math.maxInt(u32), 0); | ||
switch (errno(rc)) { | ||
0 => {}, | ||
EBADF => return error.BadFileHandle, | ||
EFBIG => return error.FileTooBig, | ||
EIO => return error.InputOutput, | ||
EISDIR => return error.IsDir, | ||
ENOMEM => return error.OutOfMemory, | ||
ENOSPC => return error.NoSpaceLeft, | ||
EOVERFLOW => return error.Unseekable, | ||
EPERM => return error.PermissionDenied, | ||
ETXTBSY => return error.FileBusy, | ||
// These may not be regular files, try fallback | ||
EINVAL => break :cfr_loop, | ||
// Support for cross-filesystem copy added in Linux 5.3, use fallback | ||
EXDEV => break :cfr_loop, | ||
// Syscall added in Linux 4.5, use fallback | ||
ENOSYS => { | ||
has_copy_file_range_syscall.set(0); | ||
break :cfr_loop; | ||
}, | ||
else => |err| return unexpectedErrno(err), | ||
} | ||
// Terminate when no data was copied | ||
if (rc == 0) return; | ||
} | ||
// This point is reached when an error occurred, hopefully no data | ||
// was transferred yet | ||
} | ||
} | ||
|
||
// 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]iovec_const{}; | ||
var offset: u64 = 0; | ||
sendfile_loop: while (true) { | ||
const amt = try sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); | ||
if (amt == 0) break :sendfile_loop; | ||
offset += amt; | ||
} | ||
} | ||
|
||
pub const PollError = error{ | ||
/// The kernel had no space to allocate file descriptor tables. | ||
SystemResources, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems this can be null: https://opensource.apple.com/source/copyfile/copyfile-166/copyfile.c.auto.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but the
alloc
/free
are now unused and can get the boot before/after merging.