Browse code

Merge pull request #35537 from sargun/vfs-use-copy_file_range

Have VFS graphdriver use accelerated in-kernel copy

Yong Tang authored on 2017/12/05 10:34:56
Showing 7 changed files
... ...
@@ -11,6 +11,7 @@ package copy
11 11
 */
12 12
 import "C"
13 13
 import (
14
+	"container/list"
14 15
 	"fmt"
15 16
 	"io"
16 17
 	"os"
... ...
@@ -65,7 +66,7 @@ func copyRegular(srcPath, dstPath string, fileinfo os.FileInfo, copyWithFileRang
65 65
 		// as the ioctl may not have been available (therefore EINVAL)
66 66
 		if err == unix.EXDEV || err == unix.ENOSYS {
67 67
 			*copyWithFileRange = false
68
-		} else if err != nil {
68
+		} else {
69 69
 			return err
70 70
 		}
71 71
 	}
... ...
@@ -106,11 +107,28 @@ func copyXattr(srcPath, dstPath, attr string) error {
106 106
 	return nil
107 107
 }
108 108
 
109
+type fileID struct {
110
+	dev uint64
111
+	ino uint64
112
+}
113
+
114
+type dirMtimeInfo struct {
115
+	dstPath *string
116
+	stat    *syscall.Stat_t
117
+}
118
+
109 119
 // DirCopy copies or hardlinks the contents of one directory to another,
110 120
 // properly handling xattrs, and soft links
111
-func DirCopy(srcDir, dstDir string, copyMode Mode) error {
121
+//
122
+// Copying xattrs can be opted out of by passing false for copyXattrs.
123
+func DirCopy(srcDir, dstDir string, copyMode Mode, copyXattrs bool) error {
112 124
 	copyWithFileRange := true
113 125
 	copyWithFileClone := true
126
+
127
+	// This is a map of source file inodes to dst file paths
128
+	copiedFiles := make(map[fileID]string)
129
+
130
+	dirsToSetMtimes := list.New()
114 131
 	err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
115 132
 		if err != nil {
116 133
 			return err
... ...
@@ -136,15 +154,21 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
136 136
 
137 137
 		switch f.Mode() & os.ModeType {
138 138
 		case 0: // Regular file
139
+			id := fileID{dev: stat.Dev, ino: stat.Ino}
139 140
 			if copyMode == Hardlink {
140 141
 				isHardlink = true
141 142
 				if err2 := os.Link(srcPath, dstPath); err2 != nil {
142 143
 					return err2
143 144
 				}
145
+			} else if hardLinkDstPath, ok := copiedFiles[id]; ok {
146
+				if err2 := os.Link(hardLinkDstPath, dstPath); err2 != nil {
147
+					return err2
148
+				}
144 149
 			} else {
145 150
 				if err2 := copyRegular(srcPath, dstPath, f, &copyWithFileRange, &copyWithFileClone); err2 != nil {
146 151
 					return err2
147 152
 				}
153
+				copiedFiles[id] = dstPath
148 154
 			}
149 155
 
150 156
 		case os.ModeDir:
... ...
@@ -192,16 +216,10 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
192 192
 			return err
193 193
 		}
194 194
 
195
-		if err := copyXattr(srcPath, dstPath, "security.capability"); err != nil {
196
-			return err
197
-		}
198
-
199
-		// We need to copy this attribute if it appears in an overlay upper layer, as
200
-		// this function is used to copy those. It is set by overlay if a directory
201
-		// is removed and then re-created and should not inherit anything from the
202
-		// same dir in the lower dir.
203
-		if err := copyXattr(srcPath, dstPath, "trusted.overlay.opaque"); err != nil {
204
-			return err
195
+		if copyXattrs {
196
+			if err := doCopyXattrs(srcPath, dstPath); err != nil {
197
+				return err
198
+			}
205 199
 		}
206 200
 
207 201
 		isSymlink := f.Mode()&os.ModeSymlink != 0
... ...
@@ -216,7 +234,9 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
216 216
 
217 217
 		// system.Chtimes doesn't support a NOFOLLOW flag atm
218 218
 		// nolint: unconvert
219
-		if !isSymlink {
219
+		if f.IsDir() {
220
+			dirsToSetMtimes.PushFront(&dirMtimeInfo{dstPath: &dstPath, stat: stat})
221
+		} else if !isSymlink {
220 222
 			aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
221 223
 			mTime := time.Unix(int64(stat.Mtim.Sec), int64(stat.Mtim.Nsec))
222 224
 			if err := system.Chtimes(dstPath, aTime, mTime); err != nil {
... ...
@@ -230,5 +250,31 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
230 230
 		}
231 231
 		return nil
232 232
 	})
233
-	return err
233
+	if err != nil {
234
+		return err
235
+	}
236
+	for e := dirsToSetMtimes.Front(); e != nil; e = e.Next() {
237
+		mtimeInfo := e.Value.(*dirMtimeInfo)
238
+		ts := []syscall.Timespec{mtimeInfo.stat.Atim, mtimeInfo.stat.Mtim}
239
+		if err := system.LUtimesNano(*mtimeInfo.dstPath, ts); err != nil {
240
+			return err
241
+		}
242
+	}
243
+
244
+	return nil
245
+}
246
+
247
+func doCopyXattrs(srcPath, dstPath string) error {
248
+	if err := copyXattr(srcPath, dstPath, "security.capability"); err != nil {
249
+		return err
250
+	}
251
+
252
+	// We need to copy this attribute if it appears in an overlay upper layer, as
253
+	// this function is used to copy those. It is set by overlay if a directory
254
+	// is removed and then re-created and should not inherit anything from the
255
+	// same dir in the lower dir.
256
+	if err := copyXattr(srcPath, dstPath, "trusted.overlay.opaque"); err != nil {
257
+		return err
258
+	}
259
+	return nil
234 260
 }
... ...
@@ -3,15 +3,20 @@
3 3
 package copy
4 4
 
5 5
 import (
6
+	"fmt"
6 7
 	"io/ioutil"
7 8
 	"math/rand"
8 9
 	"os"
9 10
 	"path/filepath"
11
+	"syscall"
10 12
 	"testing"
13
+	"time"
11 14
 
12 15
 	"github.com/docker/docker/pkg/parsers/kernel"
16
+	"github.com/docker/docker/pkg/system"
13 17
 	"github.com/stretchr/testify/assert"
14 18
 	"github.com/stretchr/testify/require"
19
+	"golang.org/x/sys/unix"
15 20
 )
16 21
 
17 22
 func TestIsCopyFileRangeSyscallAvailable(t *testing.T) {
... ...
@@ -45,6 +50,84 @@ func TestCopyWithoutRange(t *testing.T) {
45 45
 	doCopyTest(t, &copyWithFileRange, &copyWithFileClone)
46 46
 }
47 47
 
48
+func TestCopyDir(t *testing.T) {
49
+	srcDir, err := ioutil.TempDir("", "srcDir")
50
+	require.NoError(t, err)
51
+	populateSrcDir(t, srcDir, 3)
52
+
53
+	dstDir, err := ioutil.TempDir("", "testdst")
54
+	require.NoError(t, err)
55
+	defer os.RemoveAll(dstDir)
56
+
57
+	assert.NoError(t, DirCopy(srcDir, dstDir, Content, false))
58
+	require.NoError(t, filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
59
+		if err != nil {
60
+			return err
61
+		}
62
+
63
+		// Rebase path
64
+		relPath, err := filepath.Rel(srcDir, srcPath)
65
+		require.NoError(t, err)
66
+		if relPath == "." {
67
+			return nil
68
+		}
69
+
70
+		dstPath := filepath.Join(dstDir, relPath)
71
+		require.NoError(t, err)
72
+
73
+		// If we add non-regular dirs and files to the test
74
+		// then we need to add more checks here.
75
+		dstFileInfo, err := os.Lstat(dstPath)
76
+		require.NoError(t, err)
77
+
78
+		srcFileSys := f.Sys().(*syscall.Stat_t)
79
+		dstFileSys := dstFileInfo.Sys().(*syscall.Stat_t)
80
+
81
+		t.Log(relPath)
82
+		if srcFileSys.Dev == dstFileSys.Dev {
83
+			assert.NotEqual(t, srcFileSys.Ino, dstFileSys.Ino)
84
+		}
85
+		// Todo: check size, and ctim is not equal
86
+		/// on filesystems that have granular ctimes
87
+		assert.Equal(t, srcFileSys.Mode, dstFileSys.Mode)
88
+		assert.Equal(t, srcFileSys.Uid, dstFileSys.Uid)
89
+		assert.Equal(t, srcFileSys.Gid, dstFileSys.Gid)
90
+		assert.Equal(t, srcFileSys.Mtim, dstFileSys.Mtim)
91
+
92
+		return nil
93
+	}))
94
+}
95
+
96
+func randomMode(baseMode int) os.FileMode {
97
+	for i := 0; i < 7; i++ {
98
+		baseMode = baseMode | (1&rand.Intn(2))<<uint(i)
99
+	}
100
+	return os.FileMode(baseMode)
101
+}
102
+
103
+func populateSrcDir(t *testing.T, srcDir string, remainingDepth int) {
104
+	if remainingDepth == 0 {
105
+		return
106
+	}
107
+	aTime := time.Unix(rand.Int63(), 0)
108
+	mTime := time.Unix(rand.Int63(), 0)
109
+
110
+	for i := 0; i < 10; i++ {
111
+		dirName := filepath.Join(srcDir, fmt.Sprintf("srcdir-%d", i))
112
+		// Owner all bits set
113
+		require.NoError(t, os.Mkdir(dirName, randomMode(0700)))
114
+		populateSrcDir(t, dirName, remainingDepth-1)
115
+		require.NoError(t, system.Chtimes(dirName, aTime, mTime))
116
+	}
117
+
118
+	for i := 0; i < 10; i++ {
119
+		fileName := filepath.Join(srcDir, fmt.Sprintf("srcfile-%d", i))
120
+		// Owner read bit set
121
+		require.NoError(t, ioutil.WriteFile(fileName, []byte{}, randomMode(0400)))
122
+		require.NoError(t, system.Chtimes(fileName, aTime, mTime))
123
+	}
124
+}
125
+
48 126
 func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
49 127
 	dir, err := ioutil.TempDir("", "docker-copy-check")
50 128
 	require.NoError(t, err)
... ...
@@ -65,3 +148,32 @@ func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
65 65
 	require.NoError(t, err)
66 66
 	assert.Equal(t, buf, readBuf)
67 67
 }
68
+
69
+func TestCopyHardlink(t *testing.T) {
70
+	var srcFile1FileInfo, srcFile2FileInfo, dstFile1FileInfo, dstFile2FileInfo unix.Stat_t
71
+
72
+	srcDir, err := ioutil.TempDir("", "srcDir")
73
+	require.NoError(t, err)
74
+	defer os.RemoveAll(srcDir)
75
+
76
+	dstDir, err := ioutil.TempDir("", "dstDir")
77
+	require.NoError(t, err)
78
+	defer os.RemoveAll(dstDir)
79
+
80
+	srcFile1 := filepath.Join(srcDir, "file1")
81
+	srcFile2 := filepath.Join(srcDir, "file2")
82
+	dstFile1 := filepath.Join(dstDir, "file1")
83
+	dstFile2 := filepath.Join(dstDir, "file2")
84
+	require.NoError(t, ioutil.WriteFile(srcFile1, []byte{}, 0777))
85
+	require.NoError(t, os.Link(srcFile1, srcFile2))
86
+
87
+	assert.NoError(t, DirCopy(srcDir, dstDir, Content, false))
88
+
89
+	require.NoError(t, unix.Stat(srcFile1, &srcFile1FileInfo))
90
+	require.NoError(t, unix.Stat(srcFile2, &srcFile2FileInfo))
91
+	require.Equal(t, srcFile1FileInfo.Ino, srcFile2FileInfo.Ino)
92
+
93
+	require.NoError(t, unix.Stat(dstFile1, &dstFile1FileInfo))
94
+	require.NoError(t, unix.Stat(dstFile2, &dstFile2FileInfo))
95
+	assert.Equal(t, dstFile1FileInfo.Ino, dstFile2FileInfo.Ino)
96
+}
... ...
@@ -327,7 +327,7 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) (retErr
327 327
 		return err
328 328
 	}
329 329
 
330
-	return copy.DirCopy(parentUpperDir, upperDir, copy.Content)
330
+	return copy.DirCopy(parentUpperDir, upperDir, copy.Content, true)
331 331
 }
332 332
 
333 333
 func (d *Driver) dir(id string) string {
... ...
@@ -466,7 +466,7 @@ func (d *Driver) ApplyDiff(id string, parent string, diff io.Reader) (size int64
466 466
 		}
467 467
 	}()
468 468
 
469
-	if err = copy.DirCopy(parentRootDir, tmpRootDir, copy.Hardlink); err != nil {
469
+	if err = copy.DirCopy(parentRootDir, tmpRootDir, copy.Hardlink, true); err != nil {
470 470
 		return 0, err
471 471
 	}
472 472
 
473 473
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+// +build linux
1
+
2
+package vfs
3
+
4
+import "github.com/docker/docker/daemon/graphdriver/copy"
5
+
6
+func dirCopy(srcDir, dstDir string) error {
7
+	return copy.DirCopy(srcDir, dstDir, copy.Content, false)
8
+}
0 9
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+// +build !linux
1
+
2
+package vfs
3
+
4
+import "github.com/docker/docker/pkg/chrootarchive"
5
+
6
+func dirCopy(srcDir, dstDir string) error {
7
+	return chrootarchive.NewArchiver(nil).CopyWithTar(srcDir, dstDir)
8
+}
... ...
@@ -7,7 +7,6 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/daemon/graphdriver"
9 9
 	"github.com/docker/docker/daemon/graphdriver/quota"
10
-	"github.com/docker/docker/pkg/chrootarchive"
11 10
 	"github.com/docker/docker/pkg/containerfs"
12 11
 	"github.com/docker/docker/pkg/idtools"
13 12
 	"github.com/docker/docker/pkg/system"
... ...
@@ -16,8 +15,8 @@ import (
16 16
 )
17 17
 
18 18
 var (
19
-	// CopyWithTar defines the copy method to use.
20
-	CopyWithTar = chrootarchive.NewArchiver(nil).CopyWithTar
19
+	// CopyDir defines the copy method to use.
20
+	CopyDir = dirCopy
21 21
 )
22 22
 
23 23
 func init() {
... ...
@@ -133,7 +132,7 @@ func (d *Driver) create(id, parent string, size uint64) error {
133 133
 	if err != nil {
134 134
 		return fmt.Errorf("%s: %s", parent, err)
135 135
 	}
136
-	return CopyWithTar(parentDir.Path(), dir)
136
+	return CopyDir(parentDir.Path(), dir)
137 137
 }
138 138
 
139 139
 func (d *Driver) dir(id string) string {
... ...
@@ -23,7 +23,7 @@ import (
23 23
 func init() {
24 24
 	graphdriver.ApplyUncompressedLayer = archive.UnpackLayer
25 25
 	defaultArchiver := archive.NewDefaultArchiver()
26
-	vfs.CopyWithTar = defaultArchiver.CopyWithTar
26
+	vfs.CopyDir = defaultArchiver.CopyWithTar
27 27
 }
28 28
 
29 29
 func newVFSGraphDriver(td string) (graphdriver.Driver, error) {