Skip to content

Commit 6ac81fd

Browse files
committed
Merge tag 'vfs-6.13.mgtime' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs
Pull vfs multigrain timestamps from Christian Brauner: "This is another try at implementing multigrain timestamps. This time with significant help from the timekeeping maintainers to reduce the performance impact. Thomas provided a base branch that contains the required timekeeping interfaces for the VFS. It serves as the base for the multi-grain timestamp work: - Multigrain timestamps allow the kernel to use fine-grained timestamps when an inode's attributes is being actively observed via ->getattr(). With this support, it's possible for a file to get a fine-grained timestamp, and another modified after it to get a coarse-grained stamp that is earlier than the fine-grained time. If this happens then the files can appear to have been modified in reverse order, which breaks VFS ordering guarantees. To prevent this, a floor value is maintained for multigrain timestamps. Whenever a fine-grained timestamp is handed out, record it, and when later coarse-grained stamps are handed out, ensure they are not earlier than that value. If the coarse-grained timestamp is earlier than the fine-grained floor, return the floor value instead. The timekeeper changes add a static singleton atomic64_t into timekeeper.c that is used to keep track of the latest fine-grained time ever handed out. This is tracked as a monotonic ktime_t value to ensure that it isn't affected by clock jumps. Because it is updated at different times than the rest of the timekeeper object, the floor value is managed independently of the timekeeper via a cmpxchg() operation, and sits on its own cacheline. Two new public timekeeper interfaces are added: (1) ktime_get_coarse_real_ts64_mg() fills a timespec64 with the later of the coarse-grained clock and the floor time (2) ktime_get_real_ts64_mg() gets the fine-grained clock value, and tries to swap it into the floor. A timespec64 is filled with the result. - The VFS has always used coarse-grained timestamps when updating the ctime and mtime after a change. This has the benefit of allowing filesystems to optimize away a lot metadata updates, down to around 1 per jiffy, even when a file is under heavy writes. Unfortunately, this has always been an issue when we're exporting via NFSv3, which relies on timestamps to validate caches. A lot of changes can happen in a jiffy, so timestamps aren't sufficient to help the client decide when to invalidate the cache. Even with NFSv4, a lot of exported filesystems don't properly support a change attribute and are subject to the same problems with timestamp granularity. Other applications have similar issues with timestamps (e.g backup applications). If we were to always use fine-grained timestamps, that would improve the situation, but that becomes rather expensive, as the underlying filesystem would have to log a lot more metadata updates. This adds a way to only use fine-grained timestamps when they are being actively queried. Use the (unused) top bit in inode->i_ctime_nsec as a flag that indicates whether the current timestamps have been queried via stat() or the like. When it's set, we allow the kernel to use a fine-grained timestamp iff it's necessary to make the ctime show a different value. This solves the problem of being able to distinguish the timestamp between updates, but introduces a new problem: it's now possible for a file being changed to get a fine-grained timestamp. A file that is altered just a bit later can then get a coarse-grained one that appears older than the earlier fine-grained time. This violates timestamp ordering guarantees. This is where the earlier mentioned timkeeping interfaces help. A global monotonic atomic64_t value is kept that acts as a timestamp floor. When we go to stamp a file, we first get the latter of the current floor value and the current coarse-grained time. If the inode ctime hasn't been queried then we just attempt to stamp it with that value. If it has been queried, then first see whether the current coarse time is later than the existing ctime. If it is, then we accept that value. If it isn't, then we get a fine-grained time and try to swap that into the global floor. Whether that succeeds or fails, we take the resulting floor time, convert it to realtime and try to swap that into the ctime. We take the result of the ctime swap whether it succeeds or fails, since either is just as valid. Filesystems can opt into this by setting the FS_MGTIME fstype flag. Others should be unaffected (other than being subject to the same floor value as multigrain filesystems)" * tag 'vfs-6.13.mgtime' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: fs: reduce pointer chasing in is_mgtime() test tmpfs: add support for multigrain timestamps btrfs: convert to multigrain timestamps ext4: switch to multigrain timestamps xfs: switch to multigrain timestamps Documentation: add a new file documenting multigrain timestamps fs: add percpu counters for significant multigrain timestamp events fs: tracepoints around multigrain timestamp events fs: handle delegated timestamps in setattr_copy_mgtime timekeeping: Add percpu counter for tracking floor swap events timekeeping: Add interfaces for handling timestamps with a floor value fs: have setattr_copy handle multigrain timestamps appropriately fs: add infrastructure for multigrain timestamps
2 parents adc2186 + 9fed2c0 commit 6ac81fd

File tree

18 files changed

+793
-73
lines changed

18 files changed

+793
-73
lines changed

Documentation/filesystems/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ algorithms work.
2929
fiemap
3030
files
3131
locks
32+
multigrain-ts
3233
mount_api
3334
quota
3435
seq_file
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
.. SPDX-License-Identifier: GPL-2.0
2+
3+
=====================
4+
Multigrain Timestamps
5+
=====================
6+
7+
Introduction
8+
============
9+
Historically, the kernel has always used coarse time values to stamp inodes.
10+
This value is updated every jiffy, so any change that happens within that jiffy
11+
will end up with the same timestamp.
12+
13+
When the kernel goes to stamp an inode (due to a read or write), it first gets
14+
the current time and then compares it to the existing timestamp(s) to see
15+
whether anything will change. If nothing changed, then it can avoid updating
16+
the inode's metadata.
17+
18+
Coarse timestamps are therefore good from a performance standpoint, since they
19+
reduce the need for metadata updates, but bad from the standpoint of
20+
determining whether anything has changed, since a lot of things can happen in a
21+
jiffy.
22+
23+
They are particularly troublesome with NFSv3, where unchanging timestamps can
24+
make it difficult to tell whether to invalidate caches. NFSv4 provides a
25+
dedicated change attribute that should always show a visible change, but not
26+
all filesystems implement this properly, causing the NFS server to substitute
27+
the ctime in many cases.
28+
29+
Multigrain timestamps aim to remedy this by selectively using fine-grained
30+
timestamps when a file has had its timestamps queried recently, and the current
31+
coarse-grained time does not cause a change.
32+
33+
Inode Timestamps
34+
================
35+
There are currently 3 timestamps in the inode that are updated to the current
36+
wallclock time on different activity:
37+
38+
ctime:
39+
The inode change time. This is stamped with the current time whenever
40+
the inode's metadata is changed. Note that this value is not settable
41+
from userland.
42+
43+
mtime:
44+
The inode modification time. This is stamped with the current time
45+
any time a file's contents change.
46+
47+
atime:
48+
The inode access time. This is stamped whenever an inode's contents are
49+
read. Widely considered to be a terrible mistake. Usually avoided with
50+
options like noatime or relatime.
51+
52+
Updating the mtime always implies a change to the ctime, but updating the
53+
atime due to a read request does not.
54+
55+
Multigrain timestamps are only tracked for the ctime and the mtime. atimes are
56+
not affected and always use the coarse-grained value (subject to the floor).
57+
58+
Inode Timestamp Ordering
59+
========================
60+
61+
In addition to just providing info about changes to individual files, file
62+
timestamps also serve an important purpose in applications like "make". These
63+
programs measure timestamps in order to determine whether source files might be
64+
newer than cached objects.
65+
66+
Userland applications like make can only determine ordering based on
67+
operational boundaries. For a syscall those are the syscall entry and exit
68+
points. For io_uring or nfsd operations, that's the request submission and
69+
response. In the case of concurrent operations, userland can make no
70+
determination about the order in which things will occur.
71+
72+
For instance, if a single thread modifies one file, and then another file in
73+
sequence, the second file must show an equal or later mtime than the first. The
74+
same is true if two threads are issuing similar operations that do not overlap
75+
in time.
76+
77+
If however, two threads have racing syscalls that overlap in time, then there
78+
is no such guarantee, and the second file may appear to have been modified
79+
before, after or at the same time as the first, regardless of which one was
80+
submitted first.
81+
82+
Note that the above assumes that the system doesn't experience a backward jump
83+
of the realtime clock. If that occurs at an inopportune time, then timestamps
84+
can appear to go backward, even on a properly functioning system.
85+
86+
Multigrain Timestamp Implementation
87+
===================================
88+
Multigrain timestamps are aimed at ensuring that changes to a single file are
89+
always recognizable, without violating the ordering guarantees when multiple
90+
different files are modified. This affects the mtime and the ctime, but the
91+
atime will always use coarse-grained timestamps.
92+
93+
It uses an unused bit in the i_ctime_nsec field to indicate whether the mtime
94+
or ctime has been queried. If either or both have, then the kernel takes
95+
special care to ensure the next timestamp update will display a visible change.
96+
This ensures tight cache coherency for use-cases like NFS, without sacrificing
97+
the benefits of reduced metadata updates when files aren't being watched.
98+
99+
The Ctime Floor Value
100+
=====================
101+
It's not sufficient to simply use fine or coarse-grained timestamps based on
102+
whether the mtime or ctime has been queried. A file could get a fine grained
103+
timestamp, and then a second file modified later could get a coarse-grained one
104+
that appears earlier than the first, which would break the kernel's timestamp
105+
ordering guarantees.
106+
107+
To mitigate this problem, maintain a global floor value that ensures that
108+
this can't happen. The two files in the above example may appear to have been
109+
modified at the same time in such a case, but they will never show the reverse
110+
order. To avoid problems with realtime clock jumps, the floor is managed as a
111+
monotonic ktime_t, and the values are converted to realtime clock values as
112+
needed.
113+
114+
Implementation Notes
115+
====================
116+
Multigrain timestamps are intended for use by local filesystems that get
117+
ctime values from the local clock. This is in contrast to network filesystems
118+
and the like that just mirror timestamp values from a server.
119+
120+
For most filesystems, it's sufficient to just set the FS_MGTIME flag in the
121+
fstype->fs_flags in order to opt-in, providing the ctime is only ever set via
122+
inode_set_ctime_current(). If the filesystem has a ->getattr routine that
123+
doesn't call generic_fillattr, then it should call fill_mg_cmtime() to
124+
fill those values. For setattr, it should use setattr_copy() to update the
125+
timestamps, or otherwise mimic its behavior.

fs/attr.c

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,47 @@ int inode_newsize_ok(const struct inode *inode, loff_t offset)
271271
}
272272
EXPORT_SYMBOL(inode_newsize_ok);
273273

274+
/**
275+
* setattr_copy_mgtime - update timestamps for mgtime inodes
276+
* @inode: inode timestamps to be updated
277+
* @attr: attrs for the update
278+
*
279+
* With multigrain timestamps, take more care to prevent races when
280+
* updating the ctime. Always update the ctime to the very latest using
281+
* the standard mechanism, and use that to populate the atime and mtime
282+
* appropriately (unless those are being set to specific values).
283+
*/
284+
static void setattr_copy_mgtime(struct inode *inode, const struct iattr *attr)
285+
{
286+
unsigned int ia_valid = attr->ia_valid;
287+
struct timespec64 now;
288+
289+
if (ia_valid & ATTR_CTIME) {
290+
/*
291+
* In the case of an update for a write delegation, we must respect
292+
* the value in ia_ctime and not use the current time.
293+
*/
294+
if (ia_valid & ATTR_DELEG)
295+
now = inode_set_ctime_deleg(inode, attr->ia_ctime);
296+
else
297+
now = inode_set_ctime_current(inode);
298+
} else {
299+
/* If ATTR_CTIME isn't set, then ATTR_MTIME shouldn't be either. */
300+
WARN_ON_ONCE(ia_valid & ATTR_MTIME);
301+
now = current_time(inode);
302+
}
303+
304+
if (ia_valid & ATTR_ATIME_SET)
305+
inode_set_atime_to_ts(inode, attr->ia_atime);
306+
else if (ia_valid & ATTR_ATIME)
307+
inode_set_atime_to_ts(inode, now);
308+
309+
if (ia_valid & ATTR_MTIME_SET)
310+
inode_set_mtime_to_ts(inode, attr->ia_mtime);
311+
else if (ia_valid & ATTR_MTIME)
312+
inode_set_mtime_to_ts(inode, now);
313+
}
314+
274315
/**
275316
* setattr_copy - copy simple metadata updates into the generic inode
276317
* @idmap: idmap of the mount the inode was found from
@@ -303,19 +344,27 @@ void setattr_copy(struct mnt_idmap *idmap, struct inode *inode,
303344

304345
i_uid_update(idmap, attr, inode);
305346
i_gid_update(idmap, attr, inode);
306-
if (ia_valid & ATTR_ATIME)
307-
inode_set_atime_to_ts(inode, attr->ia_atime);
308-
if (ia_valid & ATTR_MTIME)
309-
inode_set_mtime_to_ts(inode, attr->ia_mtime);
310-
if (ia_valid & ATTR_CTIME)
311-
inode_set_ctime_to_ts(inode, attr->ia_ctime);
312347
if (ia_valid & ATTR_MODE) {
313348
umode_t mode = attr->ia_mode;
314349
if (!in_group_or_capable(idmap, inode,
315350
i_gid_into_vfsgid(idmap, inode)))
316351
mode &= ~S_ISGID;
317352
inode->i_mode = mode;
318353
}
354+
355+
if (is_mgtime(inode))
356+
return setattr_copy_mgtime(inode, attr);
357+
358+
if (ia_valid & ATTR_ATIME)
359+
inode_set_atime_to_ts(inode, attr->ia_atime);
360+
if (ia_valid & ATTR_MTIME)
361+
inode_set_mtime_to_ts(inode, attr->ia_mtime);
362+
if (ia_valid & ATTR_CTIME) {
363+
if (ia_valid & ATTR_DELEG)
364+
inode_set_ctime_deleg(inode, attr->ia_ctime);
365+
else
366+
inode_set_ctime_to_ts(inode, attr->ia_ctime);
367+
}
319368
}
320369
EXPORT_SYMBOL(setattr_copy);
321370

fs/btrfs/file.c

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,26 +1120,6 @@ void btrfs_check_nocow_unlock(struct btrfs_inode *inode)
11201120
btrfs_drew_write_unlock(&inode->root->snapshot_lock);
11211121
}
11221122

1123-
static void update_time_for_write(struct inode *inode)
1124-
{
1125-
struct timespec64 now, ts;
1126-
1127-
if (IS_NOCMTIME(inode))
1128-
return;
1129-
1130-
now = current_time(inode);
1131-
ts = inode_get_mtime(inode);
1132-
if (!timespec64_equal(&ts, &now))
1133-
inode_set_mtime_to_ts(inode, now);
1134-
1135-
ts = inode_get_ctime(inode);
1136-
if (!timespec64_equal(&ts, &now))
1137-
inode_set_ctime_to_ts(inode, now);
1138-
1139-
if (IS_I_VERSION(inode))
1140-
inode_inc_iversion(inode);
1141-
}
1142-
11431123
int btrfs_write_check(struct kiocb *iocb, struct iov_iter *from, size_t count)
11441124
{
11451125
struct file *file = iocb->ki_filp;
@@ -1170,7 +1150,10 @@ int btrfs_write_check(struct kiocb *iocb, struct iov_iter *from, size_t count)
11701150
* need to start yet another transaction to update the inode as we will
11711151
* update the inode when we finish writing whatever data we write.
11721152
*/
1173-
update_time_for_write(inode);
1153+
if (!IS_NOCMTIME(inode)) {
1154+
inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
1155+
inode_inc_iversion(inode);
1156+
}
11741157

11751158
start_pos = round_down(pos, fs_info->sectorsize);
11761159
oldsize = i_size_read(inode);

fs/btrfs/super.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2191,7 +2191,8 @@ static struct file_system_type btrfs_fs_type = {
21912191
.init_fs_context = btrfs_init_fs_context,
21922192
.parameters = btrfs_fs_parameters,
21932193
.kill_sb = btrfs_kill_super,
2194-
.fs_flags = FS_REQUIRES_DEV | FS_BINARY_MOUNTDATA | FS_ALLOW_IDMAP,
2194+
.fs_flags = FS_REQUIRES_DEV | FS_BINARY_MOUNTDATA |
2195+
FS_ALLOW_IDMAP | FS_MGTIME,
21952196
};
21962197

21972198
MODULE_ALIAS_FS("btrfs");

fs/ext4/super.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7329,7 +7329,7 @@ static struct file_system_type ext4_fs_type = {
73297329
.init_fs_context = ext4_init_fs_context,
73307330
.parameters = ext4_param_specs,
73317331
.kill_sb = ext4_kill_sb,
7332-
.fs_flags = FS_REQUIRES_DEV | FS_ALLOW_IDMAP,
7332+
.fs_flags = FS_REQUIRES_DEV | FS_ALLOW_IDMAP | FS_MGTIME,
73337333
};
73347334
MODULE_ALIAS_FS("ext4");
73357335

0 commit comments

Comments
 (0)