Have VFS graphdriver use accelerated in-kernel copy
| ... | ... |
@@ -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, ©WithFileRange, ©WithFileClone); 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, ©WithFileRange, ©WithFileClone) |
| 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 |
|
| ... | ... |
@@ -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) {
|