aboutsummaryrefslogtreecommitdiff
path: root/src/os
diff options
context:
space:
mode:
authorDamien Neil <dneil@google.com>2024-10-25 11:47:53 -0700
committerDamien Neil <dneil@google.com>2024-10-28 23:45:31 +0000
commit98b3be702b05e8fc071fececf7bf44f078bf032f (patch)
tree6dd003824218c5120d94be622030f7cdddf07248 /src/os
parentcd54b9bae94b36f67869ef174cbb432bc4012183 (diff)
downloadgo-98b3be702b05e8fc071fececf7bf44f078bf032f.tar.xz
os, net, internal/poll: combine unix sendfile implementations
The internal/poll/sendfile_{bsd,linux,solaris}.go implementations have more in common than not. Combine into a single sendfile_unix.go. The net and os packages have redundant code dealing with sendfile quirks on non-Linux Unix systems, such as the need to determine the size of the source file before sending. Move the common code into internal/poll. Remove some obsolete or incorrect behaviors: Drop the maximum sendfile chunk size. If we ask the kernel to copy more data than it is willing to send, it'll copy up to its limit. There was a comment in net/sendfile_unix_alt.go indicating that copying more bytes than a file contains results in the kernel looping back to the start of the file. I am unable to replicate this behavior anywhere. Dropped the comment, the workarounds, and added a test covering this case. Darwin, Dragonfly, and FreeBSD all support copying the entire contents of a file by passing 0 for the copy limit. Take advantage of this. Change-Id: I9f707ac7a27c165020ae02a6b5bb8f6f16f3c530 Reviewed-on: https://go-review.googlesource.com/c/go/+/621416 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@google.com>
Diffstat (limited to 'src/os')
-rw-r--r--src/os/copy_test.go124
-rw-r--r--src/os/zero_copy_solaris.go40
2 files changed, 113 insertions, 51 deletions
diff --git a/src/os/copy_test.go b/src/os/copy_test.go
index 82346ca4e5..407a59af42 100644
--- a/src/os/copy_test.go
+++ b/src/os/copy_test.go
@@ -7,6 +7,7 @@ package os_test
import (
"bytes"
"errors"
+ "fmt"
"io"
"math/rand/v2"
"net"
@@ -70,28 +71,135 @@ func TestLargeCopyViaNetwork(t *testing.T) {
}
}
+func TestCopyFileToFile(t *testing.T) {
+ const size = 1 * 1024 * 1024
+ dir := t.TempDir()
+
+ src, err := os.Create(dir + "/src")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer src.Close()
+ if _, err := io.CopyN(src, newRandReader(), size); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := src.Seek(0, 0); err != nil {
+ t.Fatal(err)
+ }
+
+ mustSeek := func(f *os.File, offset int64, whence int) int64 {
+ ret, err := f.Seek(offset, whence)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return ret
+ }
+
+ for _, srcStart := range []int64{0, 100, size} {
+ remaining := size - srcStart
+ for _, dstStart := range []int64{0, 200} {
+ for _, limit := range []int64{remaining, remaining - 100, size * 2} {
+ if limit < 0 {
+ continue
+ }
+ name := fmt.Sprintf("srcStart=%v/dstStart=%v/limit=%v", srcStart, dstStart, limit)
+ t.Run(name, func(t *testing.T) {
+ dst, err := os.CreateTemp(dir, "dst")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer dst.Close()
+ defer os.Remove(dst.Name())
+
+ mustSeek(src, srcStart, io.SeekStart)
+ if _, err := io.CopyN(dst, zeroReader{}, dstStart); err != nil {
+ t.Fatal(err)
+ }
+
+ var copied int64
+ if limit == 0 {
+ copied, err = io.Copy(dst, src)
+ } else {
+ copied, err = io.CopyN(dst, src, limit)
+ }
+ if limit > remaining {
+ if err != io.EOF {
+ t.Errorf("Copy: %v; want io.EOF", err)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Copy: %v; want nil", err)
+ }
+ }
+
+ wantCopied := remaining
+ if limit != 0 {
+ wantCopied = min(limit, wantCopied)
+ }
+ if copied != wantCopied {
+ t.Errorf("copied %v bytes, want %v", copied, wantCopied)
+ }
+
+ srcPos := mustSeek(src, 0, io.SeekCurrent)
+ wantSrcPos := srcStart + wantCopied
+ if srcPos != wantSrcPos {
+ t.Errorf("source position = %v, want %v", srcPos, wantSrcPos)
+ }
+
+ dstPos := mustSeek(dst, 0, io.SeekCurrent)
+ wantDstPos := dstStart + wantCopied
+ if dstPos != wantDstPos {
+ t.Errorf("destination position = %v, want %v", dstPos, wantDstPos)
+ }
+
+ mustSeek(dst, 0, io.SeekStart)
+ rr := newRandReader()
+ io.CopyN(io.Discard, rr, srcStart)
+ wantReader := io.MultiReader(
+ io.LimitReader(zeroReader{}, dstStart),
+ io.LimitReader(rr, wantCopied),
+ )
+ if err := compareReaders(dst, wantReader); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ }
+ }
+ }
+}
+
func compareReaders(a, b io.Reader) error {
bufa := make([]byte, 4096)
bufb := make([]byte, 4096)
+ off := 0
for {
na, erra := io.ReadFull(a, bufa)
- if erra != nil && erra != io.EOF {
+ if erra != nil && erra != io.EOF && erra != io.ErrUnexpectedEOF {
return erra
}
nb, errb := io.ReadFull(b, bufb)
- if errb != nil && errb != io.EOF {
+ if errb != nil && errb != io.EOF && errb != io.ErrUnexpectedEOF {
return errb
}
if !bytes.Equal(bufa[:na], bufb[:nb]) {
return errors.New("contents mismatch")
}
- if erra == io.EOF && errb == io.EOF {
+ if erra != nil && errb != nil {
break
}
+ off += len(bufa)
}
return nil
}
+type zeroReader struct{}
+
+func (r zeroReader) Read(p []byte) (int, error) {
+ clear(p)
+ return len(p), nil
+}
+
type randReader struct {
rand *rand.Rand
}
@@ -101,16 +209,8 @@ func newRandReader() *randReader {
}
func (r *randReader) Read(p []byte) (int, error) {
- var v uint64
- var n int
for i := range p {
- if n == 0 {
- v = r.rand.Uint64()
- n = 8
- }
- p[i] = byte(v & 0xff)
- v >>= 8
- n--
+ p[i] = byte(r.rand.Uint32() & 0xff)
}
return len(p), nil
}
diff --git a/src/os/zero_copy_solaris.go b/src/os/zero_copy_solaris.go
index 697a368d21..7fc9ebdada 100644
--- a/src/os/zero_copy_solaris.go
+++ b/src/os/zero_copy_solaris.go
@@ -17,13 +17,7 @@ func (f *File) writeTo(w io.Writer) (written int64, handled bool, err error) {
// readFrom is basically a refactor of net.sendFile, but adapted to work for the target of *File.
func (f *File) readFrom(r io.Reader) (written int64, handled bool, err error) {
- // SunOS uses 0 as the "until EOF" value.
- // If you pass in more bytes than the file contains, it will
- // loop back to the beginning ad nauseam until it's sent
- // exactly the number of bytes told to. As such, we need to
- // know exactly how many bytes to send.
var remain int64 = 0
-
lr, ok := r.(*io.LimitedReader)
if ok {
remain, r = lr.N, lr.R
@@ -74,25 +68,6 @@ func (f *File) readFrom(r io.Reader) (written int64, handled bool, err error) {
}
}
- if remain == 0 {
- fi, err := src.Stat()
- if err != nil {
- return 0, false, err
- }
-
- remain = fi.Size()
- }
-
- // The other quirk with SunOS' sendfile implementation
- // is that it doesn't use the current position of the file
- // -- if you pass it offset 0, it starts from offset 0.
- // There's no way to tell it "start from current position",
- // so we have to manage that explicitly.
- pos, err := src.Seek(0, io.SeekCurrent)
- if err != nil {
- return
- }
-
sc, err := src.SyscallConn()
if err != nil {
return
@@ -103,28 +78,15 @@ func (f *File) readFrom(r io.Reader) (written int64, handled bool, err error) {
// https://docs.oracle.com/cd/E88353_01/html/E37843/sendfile-3c.html and
// https://illumos.org/man/3EXT/sendfile for more details.
rerr := sc.Read(func(fd uintptr) bool {
- written, err, handled = poll.SendFile(&f.pfd, int(fd), pos, remain)
+ written, err, handled = poll.SendFile(&f.pfd, int(fd), remain)
return true
})
if lr != nil {
lr.N = remain - written
}
-
- // This is another quirk on SunOS: sendfile() claims to support
- // out_fd as a regular file but returns EINVAL when the out_fd is not a
- // socket of SOCK_STREAM, while it actually sends out data anyway and updates
- // the file offset. In this case, we can just ignore the error.
- if err == syscall.EINVAL && written > 0 {
- err = nil
- }
if err == nil {
err = rerr
}
- _, err1 := src.Seek(written, io.SeekCurrent)
- if err1 != nil && err == nil {
- return written, handled, err1
- }
-
return written, handled, wrapSyscallError("sendfile", err)
}