Re-use the same device file inode in tmpfs for overlay whiteout files.

When using overlayfs with tmpfs as the upper layer (common case), depending on
the application, a lot of whiteouts can be created. This leads to a lot of
memory allocation because new dentry and inode structs need to be allocated for
each whiteout. With this change, we at least avoid the inode allocations.

This is analogous with what Linux does. See fs/overlayfs/ovl_entry.h:ovl_fs's
field `whiteout` with comment "Shared whiteout cache".

PiperOrigin-RevId: 704897805
This commit is contained in:
Ayush Ranjan
2024-12-10 17:11:22 -08:00
committed by gVisor bot
parent 5166d261a9
commit ab9d8455d4
6 changed files with 44 additions and 17 deletions
+6
View File
@@ -121,3 +121,9 @@ const (
RENAME_EXCHANGE = (1 << 1) // Exchange src and dst.
RENAME_WHITEOUT = (1 << 2) // Whiteout src.
)
// Overlayfs constants from include/linux/fs.h.
const (
WHITEOUT_MODE = 0
WHITEOUT_DEV = 0
)
+1 -1
View File
@@ -83,7 +83,7 @@ func (d *dentry) collectWhiteoutsForRmdirLocked(ctx context.Context) (map[string
readdirErr = err
return false
}
if stat.RdevMajor != 0 || stat.RdevMinor != 0 {
if linux.MakeDeviceID(uint16(stat.RdevMajor), stat.RdevMinor) != linux.WHITEOUT_DEV {
// This file is a real character device, not a whiteout.
readdirErr = linuxerr.ENOTEMPTY
return false
+4 -2
View File
@@ -574,9 +574,11 @@ func (fs *filesystem) doCreateAt(ctx context.Context, rp *vfs.ResolvingPath, ct
//
// Preconditions: pop's parent directory has been copied up.
func CreateWhiteout(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, pop *vfs.PathOperation) error {
major, minor := linux.DecodeDeviceID(linux.WHITEOUT_DEV)
return vfsObj.MknodAt(ctx, creds, pop, &vfs.MknodOptions{
Mode: linux.S_IFCHR, // permissions == include/linux/fs.h:WHITEOUT_MODE == 0
// DevMajor == DevMinor == 0, from include/linux/fs.h:WHITEOUT_DEV
Mode: linux.S_IFCHR | linux.WHITEOUT_MODE,
DevMajor: uint32(major),
DevMinor: minor,
})
}
+25 -8
View File
@@ -31,21 +31,38 @@ type deviceFile struct {
minor uint32
}
func (fs *filesystem) newDeviceFile(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, kind vfs.DeviceKind, major, minor uint32, parentDir *directory) *inode {
func isOvlWhiteoutDev(mode linux.FileMode, major, minor uint32) bool {
return mode.FileType() == linux.S_IFCHR &&
mode.Permissions() == linux.WHITEOUT_MODE &&
linux.MakeDeviceID(uint16(major), minor) == linux.WHITEOUT_DEV
}
// Precondition: fs.mu must be locked for writing.
func (fs *filesystem) newDeviceFileLocked(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, major, minor uint32, parentDir *directory) *inode {
ovlWhiteout := isOvlWhiteoutDev(mode, major, minor)
if ovlWhiteout && fs.ovlWhiteout != nil {
// If reusing the same inode, acts like a hard link.
fs.ovlWhiteout.inode.incLinksLocked()
return &fs.ovlWhiteout.inode
}
file := &deviceFile{
kind: kind,
major: major,
minor: minor,
}
switch kind {
case vfs.BlockDevice:
mode |= linux.S_IFBLK
case vfs.CharDevice:
mode |= linux.S_IFCHR
switch mode.FileType() {
case linux.S_IFBLK:
file.kind = vfs.BlockDevice
case linux.S_IFCHR:
file.kind = vfs.CharDevice
default:
panic(fmt.Sprintf("invalid DeviceKind: %v", kind))
panic(fmt.Sprintf("invalid file type for device file: %s", mode))
}
file.inode.init(file, fs, kuid, kgid, mode, parentDir)
file.inode.nlink = atomicbitops.FromUint32(1) // from parent directory
if ovlWhiteout {
fs.ovlWhiteout = file
// An extra link is held by fs, so nlink doesn't fall to 0.
file.inode.incLinksLocked()
}
return &file.inode
}
+2 -4
View File
@@ -320,10 +320,8 @@ func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts v
childInode = fs.newRegularFile(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, parentDir)
case linux.S_IFIFO:
childInode = fs.newNamedPipe(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, parentDir)
case linux.S_IFBLK:
childInode = fs.newDeviceFile(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, vfs.BlockDevice, opts.DevMajor, opts.DevMinor, parentDir)
case linux.S_IFCHR:
childInode = fs.newDeviceFile(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, vfs.CharDevice, opts.DevMajor, opts.DevMinor, parentDir)
case linux.S_IFBLK, linux.S_IFCHR:
childInode = fs.newDeviceFileLocked(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, opts.DevMajor, opts.DevMinor, parentDir)
case linux.S_IFSOCK:
childInode = fs.newSocketFile(creds.EffectiveKUID, creds.EffectiveKGID, opts.Mode, opts.Endpoint, parentDir)
default:
+6 -2
View File
@@ -105,6 +105,9 @@ type filesystem struct {
// allowXattrPrefix is a set of xattr namespace prefixes that this
// tmpfs mount will allow. It is immutable.
allowXattrPrefix map[string]struct{}
// ovlWhiteout is the shared overlay whiteout device. It is protected by mu.
ovlWhiteout *deviceFile
}
// Name implements vfs.FilesystemType.Name.
@@ -328,6 +331,9 @@ func (fs *filesystem) Release(ctx context.Context) {
if fs.root.inode.isDir() {
fs.root.releaseChildrenLocked(ctx)
}
if fs.ovlWhiteout != nil {
fs.ovlWhiteout.inode.decLinksLocked(ctx)
}
fs.mu.Unlock()
if fs.mf.RestoreID() != "" {
// If RestoreID is set, then this is a private MemoryFile which needs to be
@@ -533,7 +539,6 @@ func (i *inode) init(impl any, fs *filesystem, kuid auth.KUID, kgid auth.KGID, m
//
// Preconditions:
// - filesystem.mu must be locked for writing.
// - i.mu must be lcoked.
// - i.nlink != 0.
// - i.nlink < maxLinks.
func (i *inode) incLinksLocked() {
@@ -551,7 +556,6 @@ func (i *inode) incLinksLocked() {
//
// Preconditions:
// - filesystem.mu must be locked for writing.
// - i.mu must be lcoked.
// - i.nlink != 0.
func (i *inode) decLinksLocked(ctx context.Context) {
if i.nlink.RacyLoad() == 0 {