From 90c8d36afe5c5b6ff181d27c1389990497ffc6ab Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Fri, 22 Sep 2023 15:51:36 +1000 Subject: [PATCH] dmz: use sendfile(2) when cloning /proc/self/exe This results in a 5-20% speedup of dmz.CloneBinary(), depending on the machine. io.Copy: goos: linux goarch: amd64 pkg: github.com/opencontainers/runc/libcontainer/dmz cpu: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz BenchmarkCloneBinary BenchmarkCloneBinary-8 139 8075074 ns/op PASS ok github.com/opencontainers/runc/libcontainer/dmz 2.286s unix.Sendfile: goos: linux goarch: amd64 pkg: github.com/opencontainers/runc/libcontainer/dmz cpu: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz BenchmarkCloneBinary BenchmarkCloneBinary-8 192 6382121 ns/op PASS ok github.com/opencontainers/runc/libcontainer/dmz 2.415s Signed-off-by: Aleksa Sarai --- libcontainer/dmz/cloned_binary_linux.go | 2 +- libcontainer/system/linux.go | 40 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/libcontainer/dmz/cloned_binary_linux.go b/libcontainer/dmz/cloned_binary_linux.go index 133bdefaff7..db5e18a3260 100644 --- a/libcontainer/dmz/cloned_binary_linux.go +++ b/libcontainer/dmz/cloned_binary_linux.go @@ -179,7 +179,7 @@ func CloneBinary(src io.Reader, size int64, name, tmpDir string) (*os.File, erro if err != nil { return nil, err } - copied, err := io.Copy(file, src) + copied, err := system.Copy(file, src) if err != nil { file.Close() return nil, fmt.Errorf("copy binary: %w", err) diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go index 8f52496e9bb..318b6edfe81 100644 --- a/libcontainer/system/linux.go +++ b/libcontainer/system/linux.go @@ -5,6 +5,7 @@ package system import ( "fmt" + "io" "os" "os/exec" "strconv" @@ -174,3 +175,42 @@ func ExecutableMemfd(comment string, flags int) (*os.File, error) { } return os.NewFile(uintptr(memfd), "/memfd:"+comment), nil } + +// Copy is like io.Copy except it uses sendfile(2) if the source and sink are +// both (*os.File) as an optimisation to make copies faster. +func Copy(dst io.Writer, src io.Reader) (copied int64, err error) { + dstFile, _ := dst.(*os.File) + srcFile, _ := src.(*os.File) + + if dstFile != nil && srcFile != nil { + fi, err := srcFile.Stat() + if err != nil { + goto fallback + } + size := fi.Size() + for size > 0 { + n, err := unix.Sendfile(int(dstFile.Fd()), int(srcFile.Fd()), nil, int(size)) + if n > 0 { + size -= int64(n) + copied += int64(n) + } + if err == unix.EINTR { + continue + } + if err != nil { + if copied == 0 { + // If we haven't copied anything so far, we can safely just + // fallback to io.Copy. We could always do the fallback but + // it's safer to error out in the case of a partial copy + // followed by an error (which should never happen). + goto fallback + } + return copied, fmt.Errorf("partial sendfile copy: %w", err) + } + } + return copied, nil + } + +fallback: + return io.Copy(dst, src) +}