diff options
| author | Damien Neil <dneil@google.com> | 2024-10-25 11:47:53 -0700 |
|---|---|---|
| committer | Damien Neil <dneil@google.com> | 2024-10-28 23:45:31 +0000 |
| commit | 98b3be702b05e8fc071fececf7bf44f078bf032f (patch) | |
| tree | 6dd003824218c5120d94be622030f7cdddf07248 /src/os | |
| parent | cd54b9bae94b36f67869ef174cbb432bc4012183 (diff) | |
| download | go-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.go | 124 | ||||
| -rw-r--r-- | src/os/zero_copy_solaris.go | 40 |
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) } |
